Adapter — що це, патерн перетворення інтерфейсів для мобільної розробки

Автор: IT Sectr Опубліковано: 2026-02-18 Час читання: 10 хв

Adapter — це структурний патерн проєктування, що перетворює інтерфейс одного класу в інший інтерфейс, очікуваний клієнтом. У мобільній розробці Adapter найчастіше використовується в RecyclerView.Adapter для Android та UITableViewDataSource для iOS. За даними Google I/O (2024), RecyclerView використовується в 95% Android-додатків для відображення списків, і кожен потребує власної реалізації Adapter.

Головне

  • Adapter — структурний патерн, що дозволяє об'єктам із несумісними інтерфейсами працювати разом
  • RecyclerView.Adapter — реалізація патерну в Android, що перетворює дані на ViewHolder для списку
  • UITableViewDataSource — реалізація в iOS, що об'єднує патерни Adapter та DataSource
  • Object Adapter використовує композицію: адаптер містить посилання на адаптований об'єкт
  • Class Adapter використовує множинне успадкування — можливий лише в C++, недоступний у Java/Swift/Kotlin

Що таке патерн Adapter?

Adapter — структурний патерн, що вирішує проблему несумісності інтерфейсів. Він обгортає один об'єкт (Adaptee) у клас (Adapter) з інтерфейсом, очікуваним клієнтом (Target). Клієнт працює з Target, не знаючи про існування Adaptee. Це різновид патерну Wrapper.

Kotlin
// Існуючий клас з несумісним інтерфейсом
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 десятирічної давності. Це дозволяє замінювати реалізації без зміни коду клієнта.

Adapter в Android: RecyclerView та ListAdapter

RecyclerView.Adapter — найпоширеніша реалізація патерну Adapter в Android. Він перетворює дані (список об'єктів) на ViewHolder'и, які RecyclerView відображає на екрані. З появою ListAdapter (Android Architecture Components) патерн отримав вбудовану підтримку diff-розрахунку для анімації змін.

Kotlin
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().

Adapter в iOS: UITableView та DiffableDataSource

UITableViewDataSource — це одночасно і патерн Adapter, і DataSource. Він перетворює дані моделі в комірки таблиці. З iOS 13 Apple представила DiffableDataSource — сучасну заміну, яка, подібно до ListAdapter в Android, автоматично обчислює зміни та анімує оновлення.

Swift
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 vs Class Adapter

Object Adapter використовує композицію: адаптер містить посилання на об'єкт Adaptee та делегує йому виклики. Class Adapter використовує успадкування: адаптер успадковує Adaptee та реалізує цільовий інтерфейс одночасно. В Java, Swift та Kotlin Class Adapter неможливий через відсутність множинного успадкування — залишається лише Object Adapter.

ХарактеристикаObject AdapterClass Adapter
МеханізмКомпозиція (містить Adaptee)Успадкування (розширює Adaptee)
ГнучкістьВища — Adaptee можна змінити в runtimeНижча — Adaptee фіксований на етапі компіляції
Зв'язністьСлабка (через інтерфейс)Сильна (через успадкування)
Адаптація підкласівТак — Adaptee може бути будь-яким підкласомНі — лише конкретний суперклас
ДоступністьJava, Kotlin, Swift, C++, C#Лише мови з множинним успадкуванням (C++)

У мобільній розробці використовується лише Object Adapter. RecyclerView.Adapter містить посилання на дані та LayoutInflater, UITableViewDataSource містить масив елементів. Композиція робить код більш гнучким і тестованим порівняно з успадкуванням.

Приклади Adapter в реальних проєктах

Adapter застосовується не лише для списків. Розглянемо три сценарії з практики мобільної розробки, де патерн вирішує задачу інтеграції несумісних компонентів.

СценарійAdapteeTargetAdapter
Інтеграція старої auth-бібліотекиLegacyAuthLib (callback-based)AuthService (suspend)LegacyAuthAdapter
Адаптація JSON під нову модельGsonParserJsonParser (Kotlinx Serialization)GsonAdapter
Єдиний інтерфейс для push-сервісівFCM, APNs, Huawei PushPushService token registerPushServiceAdapter

Кожен Adapter інкапсулює логіку перетворення: GsonAdapter переводить JSON-рядок в об'єкт через @SerializedName, PushServiceAdapter абстрагує реєстрацію токена для різних платформ. Клієнт (бізнес-логіка) працює з єдиним інтерфейсом і не залежить від конкретної реалізації.

Типові помилки при реалізації Adapter

Помилки в реалізації Adapter призводять до гальмування списків, витоків пам'яті та багів з оновленням даних. Розберемо три часті проблеми.

Важкі операції в onBindViewHolder

onBindViewHolder викликається для кожної комірки при скролі. Створення об'єктів, парсинг JSON або виклик БД у цьому методі призводять до смикання списку (frame drops). Рішення: всі важкі обчислення проводити заздалегідь, передаючи в адаптер уже готові дані. Для зображень використовувати бібліотеки з кешуванням — Glide, Coil, Kingfisher.

Ігнорування DiffUtil / DiffableDataSource

Виклик notifyDataSetChanged() при кожному оновленні перемальовує весь список, викликаючи мерехтіння та втрату фокусу на вводах. Рішення: використовувати ListAdapter з DiffUtil в Android або DiffableDataSource в iOS. Diff-розрахунок займає <1 мс для списків до 1000 елементів і забезпечує плавні анімації.

Зберігання важких об'єктів в адаптері

Адаптер живе стільки ж, скільки RecyclerView/UITableView. Зберігання Bitmap або великих масивів в адаптері призводить до витоків пам'яті при зміні конфігурації (поворот екрана, зміна теми). Рішення: адаптер повинен зберігати лише легкі моделі даних (data class / struct), а важкі ресурси завантажувати через ViewHolder за запитом.

Часто задавані питання

В чому різниця між Adapter та Proxy?

Adapter перетворює інтерфейс Adaptee на інтерфейс Target — клієнт отримує новий спосіб взаємодії. Proxy надає той самий інтерфейс, що й оригінальний об'єкт, але додає контроль доступу, кешування або ліниву ініціалізацію. Proxy не змінює інтерфейс, Adapter — змінює.

Чому RecyclerView.Adapter — це приклад патерну Adapter?

RecyclerView.Adapter перетворює дані (наприклад, список User) у ViewHolder, який RecyclerView може відобразити. RecyclerView очікує ViewHolder, дані мають формат List<User> — Adapter адаптує одне до іншого. Додатково Adapter керує життєвим циклом ViewHolder через recycle-пул, підвищуючи продуктивність списку.

В чому різниця між Object Adapter та Class Adapter?

Object Adapter використовує композицію: містить посилання на Adaptee та делегує йому виклики. Class Adapter використовує множинне успадкування: успадковує Adaptee та реалізує Target. В Java, Kotlin та Swift доступний лише Object Adapter через відсутність множинного успадкування класів. Class Adapter можливий лише в C++.

Коли використовувати Adapter замість Facade?

Adapter використовується, коли потрібно змусити два існуючих класи працювати разом, перетворюючи інтерфейс одного з них. Facade — коли потрібно спростити взаємодію зі складною підсистемою, надаючи простий інтерфейс. Adapter — для сумісності, Facade — для спрощення.

Чи можна реалізувати Adapter без інтерфейсу?

Так, за допомогою замикань. Замість інтерфейсу з одним методом можна передати функцію. Наприклад, в Swift: let adapter = { (data: Data) -> [CellConfig] in /* перетворення */ }. Такий підхід називається функціональним адаптером. Однак для 2+ методів протокол/інтерфейс залишається кращим — він робить контракт явним.

Підсумки

  • Adapter — структурний патерн, що перетворює несумісний інтерфейс Adaptee в Target через клас-прокладку
  • RecyclerView.Adapter (Android) та UITableViewDataSource (iOS) — головні приклади патерну в мобільній розробці
  • Object Adapter використовує композицію; Class Adapter (лише C++) — множинне успадкування
  • ListAdapter + DiffUtil (Android) та DiffableDataSource (iOS) — сучасні еволюції з автоматичним diff та анімацією
  • Важкі операції в onBindViewHolder, ігнорування DiffUtil та зберігання ресурсів в адаптері — часті помилки
  • Adapter застосовується для інтеграції legacy-коду, адаптації форматів та уніфікації різних реалізацій
  • Рекомендація: використовуйте ListAdapter/DiffableDataSource замість ручного керування оновленнями списку

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також