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) патерн отримав вбудовану підтримку diff-розрахунку для анімації змін.
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, але за однією логікою — diff-розрахунок + автоматична анімація.
Object Adapter використовує композицію: адаптер містить посилання на об'єкт Adaptee та делегує йому виклики. Class Adapter використовує успадкування: адаптер успадковує Adaptee та реалізує цільовий інтерфейс одночасно. В Java, Swift та Kotlin Class Adapter неможливий через відсутність множинного успадкування — залишається лише Object Adapter.
| Характеристика | Object Adapter | Class Adapter |
|---|---|---|
| Механізм | Композиція (містить Adaptee) | Успадкування (розширює Adaptee) |
| Гнучкість | Вища — Adaptee можна змінити в runtime | Нижча — 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. Diff-розрахунок займає <1 мс для списків до 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 через recycle-пул, підвищуючи продуктивність списку.
Object Adapter використовує композицію: містить посилання на Adaptee та делегує йому виклики. Class Adapter використовує множинне успадкування: успадковує Adaptee та реалізує Target. В Java, Kotlin та Swift доступний лише Object Adapter через відсутність множинного успадкування класів. Class Adapter можливий лише в C++.
Adapter використовується, коли потрібно змусити два існуючих класи працювати разом, перетворюючи інтерфейс одного з них. Facade — коли потрібно спростити взаємодію зі складною підсистемою, надаючи простий інтерфейс. Adapter — для сумісності, Facade — для спрощення.
Так, за допомогою замикань. Замість інтерфейсу з одним методом можна передати функцію. Наприклад, в Swift: let adapter = { (data: Data) -> [CellConfig] in /* перетворення */ }. Такий підхід називається функціональним адаптером. Однак для 2+ методів протокол/інтерфейс залишається кращим — він робить контракт явним.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також