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 или legacy 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) | Наследование (extends 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 /* преобразование */ }. Такой подход называется functional adapter. Однако для 2+ методов протокол/интерфейс остаётся предпочтительным — он делает контракт явным.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также