Adapter — е структурен шаблон за проектиране, който преобразува интерфейса на един клас в друг интерфейс, очакван от клиента. В мобилното разработване Adapter най-често се използва в RecyclerView.Adapter за Android и UITableViewDataSource за iOS. Според данни от Google I/O (2024), RecyclerView се използва в 95% от Android приложенията за показване на списъци и всяко изисква собствена реализация на Adapter.
Основни точки
Adapter — структурен шаблон, решаващ проблема с несъвместимостта на интерфейси. Той обвива един обект (Adaptee) в клас (Adapter) с интерфейс, очакван от клиента (Target). Клиентът работи с Target, без да знае за съществуването на Adaptee. Това е разновидност на шаблона Wrapper.
// Съществуващ клас с несъвместим интерфейс
class LegacyAuthApi {
fun loginWithToken(token: String): Map {
return mapOf("status" to "ok", "user_id" to 42)
}
}
// Целеви интерфейс (какво очаква клиентът)
interface AuthService {
suspend fun login(credentials: Credentials): Result
}
// Adapter — преобразува LegacyAuthApi в AuthService
class LegacyAuthAdapter(
private val legacyApi: LegacyAuthApi
) : AuthService {
override suspend fun login(credentials: Credentials): Result {
val token = "${credentials.login}:${credentials.password}"
.encodeToByteArray().let { Base64.encodeToString(it) }
val response = legacyApi.loginWithToken(token)
return if (response["status"] == "ok") {
Result.success(User(response["user_id"] as Int))
} else {
Result.failure(AuthException("Login failed"))
}
}
}LegacyAuthAdapter преобразува извикването login(credentials) в loginWithToken(token) за старото API. Клиентът (ViewModel) работи чрез интерфейса AuthService и не знае дали под него е най-новият Firebase Auth или десет години старо API. Това позволява замяна на реализации без промяна на клиентския код.
RecyclerView.Adapter — най-разпространената реализация на шаблона Adapter в Android. Той преобразува данни (списък от обекти) в ViewHolder-и, които RecyclerView показва на екрана. С появата на ListAdapter (Android Architecture Components) шаблонът получи вградена поддръжка за изчисляване на разлики за анимация на промени.
class UserAdapter(
private val onItemClick: (User) -> Unit
) : ListAdapter (DiffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(
holder: UserViewHolder,
position: Int
) {
holder.bind(getItem(position), onItemClick)
}
class UserViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
private val tvName = itemView.findViewById(R.id.tvName)
fun bind(user: User, click: (User) -> Unit) {
tvName.text = user.name
itemView.setOnClickListener { click(user) }
}
}
private class DiffCallback : DiffUtil.ItemCallback () {
override fun areItemsTheSame(old: User, new: User) = old.id == new.id
override fun areContentsTheSame(old: User, new: User) = old == new
}
} UserAdapter използва ListAdapter с DiffUtil за ефективно прерисуване на списъка. Когато данните се актуализират, DiffCallback изчислява разликата между стария и новия списък, а RecyclerView анимира само променените елементи — без прерисуване на целия списък. Това е ключово предимство пред по-стария ListView, който изискваше ръчно notifyDataSetChanged().
UITableViewDataSource — е едновременно шаблон Adapter и DataSource. Той преобразува данните на модела в клетки на таблицата. От iOS 13 Apple представи DiffableDataSource — модерна замяна, която подобно на ListAdapter в Android автоматично изчислява промените и анимира актуализациите.
class UserTableViewAdapter: NSObject, UITableViewDataSource {
var items: [User] = []
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "UserCell", for: indexPath)
let user = items[indexPath.row]
cell.textLabel?.text = user.name
return cell
}
}
// Съвременна версия: DiffableDataSource
enum Section { case main }
class ModernUserAdapter {
var dataSource: UITableViewDiffableDataSource<Section, User>!
func configure(for tableView: UITableView) {
dataSource = UITableViewDiffableDataSource(
tableView: tableView
) { tableView, indexPath, user in
let cell = tableView.dequeueReusableCell(
withIdentifier: "UserCell", for: indexPath)
cell.textLabel?.text = user.name
return cell
}
}
func update(with users: [User]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, User>()
snapshot.appendSections([.main])
snapshot.appendItems(users)
dataSource.apply(snapshot, animatingDifferences: true)
}
}ModernUserAdapter с DiffableDataSource автоматично анимира промени при актуализиране на данни: елементите се появяват, изчезват или преместват с плавни анимации. iOS DiffableDataSource и Android ListAdapter решават една и съща задача с различни API, но с една и съща логика — изчисляване на разлики + автоматична анимация.
Object Adapter използва композиция: адаптерът съдържа референция към обекта Adaptee и делегира извиквания на него. Class Adapter използва наследяване: адаптерът наследява Adaptee и едновременно реализира целевия интерфейс. В Java, Swift и Kotlin Class Adapter е невъзможен поради липсата на множествено наследяване — остава само Object Adapter.
| Характеристика | Object Adapter | Class Adapter |
|---|---|---|
| Механизъм | Композиция (съдържа Adaptee) | Наследяване (extends Adaptee) |
| Гъвкавост | По-висока — Adaptee може да се променя по време на изпълнение | По-ниска — Adaptee е фиксиран по време на компилация |
| Свързаност | Слаба (чрез интерфейс) | Силна (чрез наследяване) |
| Адаптиране на подкласове | Да — Adaptee може да бъде произволен подклас | Не — само конкретен суперклас |
| Достъпност | Java, Kotlin, Swift, C++, C# | Само езици с множествено наследяване (C++) |
В мобилното разработване се използва само Object Adapter. RecyclerView.Adapter съдържа референции към данни и LayoutInflater, UITableViewDataSource съдържа масив от елементи. Композицията прави кода по-гъвкав и тестваем в сравнение с наследяването.
Adapter се прилага не само за списъци. Нека разгледаме три сценария от практиката на мобилното разработване, където шаблонът решава проблема с интегрирането на несъвместими компоненти.
| Сценарий | Adaptee | Target | Adapter |
|---|---|---|---|
| Интегриране на стара auth библиотека | LegacyAuthLib (callback-based) | AuthService (suspend) | LegacyAuthAdapter |
| Адаптиране на JSON към нов модел | GsonParser | JsonParser (Kotlinx Serialization) | GsonAdapter |
| Единен интерфейс за push услуги | FCM, APNs, Huawei Push | PushService token register | PushServiceAdapter |
Всеки Adapter капсулира логиката на преобразуване: GsonAdapter превежда JSON низ в обект чрез @SerializedName, PushServiceAdapter абстрахира регистрацията на токен за различни платформи. Клиентът (бизнес логика) работи с единен интерфейс и не зависи от конкретна реализация.
Грешки в реализацията на Adapter водят до забавяне на списъци, изтичане на памет и грешки при актуализиране на данни. Нека разгледаме три често срещани проблема.
onBindViewHolder се извиква за всяка клетка при скролване. Създаване на обекти, парсване на JSON или заявки към база данни в този метод водят до дърпане на списъка (frame drops). Решение: всички тежки изчисления да се извършват предварително, като се предават готови данни на адаптера. За изображения използвайте библиотеки с кеширане — Glide, Coil, Kingfisher.
Извикването на notifyDataSetChanged() при всяка актуализация прерисува целия списък, причинявайки мигане и загуба на фокус върху полетата за въвеждане. Решение: използвайте ListAdapter с DiffUtil в Android или DiffableDataSource в iOS. Изчисляването на разлики отнема <1 ms за списъци до 1000 елемента и осигурява плавни анимации.
Адаптерът живее колкото RecyclerView/UITableView. Съхранението на Bitmap или големи масиви в адаптера води до изтичане на памет при промяна на конфигурацията (завъртане на екрана, смяна на тема). Решение: адаптерът трябва да съхранява само леки модели данни (data class / struct), а тежките ресурси да зарежда чрез ViewHolder при поискване.
Често задавани въпроси
Adapter преобразува интерфейса на Adaptee в интерфейса Target — клиентът получава нов начин на взаимодействие. Proxy предоставя същия интерфейс като оригиналния обект, но добавя контрол на достъпа, кеширане или мързелива инициализация. Proxy не променя интерфейса, Adapter — променя.
RecyclerView.Adapter преобразува данни (например списък от User) в ViewHolder, който RecyclerView може да покаже. RecyclerView очаква ViewHolder, данните са във формат List<User> — Adapter адаптира едното към другото. Допълнително Adapter управлява жизнения цикъл на ViewHolder чрез рециклиращ пул, повишавайки производителността на списъка.
Object Adapter използва композиция: съдържа референция към Adaptee и делегира извиквания на него. Class Adapter използва множествено наследяване: наследява Adaptee и реализира Target. В Java, Kotlin и Swift е достъпен само Object Adapter поради липсата на множествено наследяване на класове. Class Adapter е възможен само в C++.
Adapter се използва, когато два съществуващи класа трябва да работят заедно чрез преобразуване на интерфейса на единия от тях. Facade — когато взаимодействието със сложна подсистема трябва да се опрости чрез предоставяне на прост интерфейс. Adapter — за съвместимост, Facade — за опростяване.
Да, с помощта на затваряния (closures). Вместо интерфейс с един метод може да се предаде функция. Например в Swift: let adapter = { (data: Data) -> [CellConfig] in /* преобразуване */ }. Този подход се нарича функционален адаптер. Въпреки това за 2+ метода протоколът/интерфейсът остава за предпочитане — прави договора явен.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също