Adapter — to strukturalny wzorzec projektowy, który przekształca interfejs jednej klasy na inny interfejs oczekiwany przez klienta. W programowaniu mobilnym Adapter najczęściej używany jest w RecyclerView.Adapter dla Android i UITableViewDataSource dla iOS. Według danych Google I/O (2024), RecyclerView jest używany w 95% aplikacji Android do wyświetlania list, a każda wymaga własnej implementacji Adapter.
Najważniejsze
Adapter — wzorzec strukturalny rozwiązujący problem niekompatybilności interfejsów. Opakowuje jeden obiekt (Adaptee) w klasę (Adapter) z interfejsem oczekiwanym przez klienta (Target). Klient pracuje z Target, nie wiedząc o istnieniu Adaptee. Jest to odmiana wzorca Wrapper.
// Istniejąca klasa z niekompatybilnym interfejsem
class LegacyAuthApi {
fun loginWithToken(token: String): Map {
return mapOf("status" to "ok", "user_id" to 42)
}
}
// Docelowy interfejs (czego oczekuje klient)
interface AuthService {
suspend fun login(credentials: Credentials): Result
}
// Adapter — przekształca LegacyAuthApi w 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 przekształca wywołanie login(credentials) w loginWithToken(token) dla starego API. Klient (ViewModel) pracuje przez interfejs AuthService i nie wie, że pod spodem znajduje się najnowszy Firebase Auth lub legacy API sprzed dziesięciu lat. Pozwala to wymieniać implementacje bez zmiany kodu klienta.
RecyclerView.Adapter — najczęstsza implementacja wzorca Adapter w Android. Przekształca dane (listę obiektów) w ViewHolder'y, które RecyclerView wyświetla na ekranie. Wraz z pojawieniem się ListAdapter (Android Architecture Components) wzorzec otrzymał wbudowane wsparcie obliczania różnic do animacji zmian.
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 używa ListAdapter z DiffUtil do efektywnego przerysowywania listy. Gdy dane są aktualizowane, DiffCallback oblicza różnicę między starą a nową listą, a RecyclerView animuje tylko zmienione elementy — bez przerysowywania całej listy. To kluczowa zaleta w porównaniu ze starszym ListView, który wymagał ręcznego notifyDataSetChanged().
UITableViewDataSource — to jednocześnie wzorzec Adapter i DataSource. Przekształca dane modelu w komórki tabeli. Od iOS 13 Apple wprowadziło DiffableDataSource — nowoczesny zamiennik, który podobnie jak ListAdapter w Android, automatycznie oblicza zmiany i animuje aktualizacje.
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
}
}
// Nowoczesna wersja: 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 z DiffableDataSource automatycznie animuje zmiany przy aktualizacji danych: elementy pojawiają się, znikają lub przesuwają z płynnymi animacjami. iOS DiffableDataSource i Android ListAdapter rozwiązują to samo zadanie różnymi API, ale według tej samej logiki — obliczanie różnic + automatyczna animacja.
Object Adapter wykorzystuje kompozycję: adapter zawiera odniesienie do obiektu Adaptee i deleguje mu wywołania. Class Adapter wykorzystuje dziedziczenie: adapter dziedziczy Adaptee i jednocześnie implementuje docelowy interfejs. W Java, Swift i Kotlin Class Adapter jest niemożliwy z powodu braku wielodziedziczenia — pozostaje tylko Object Adapter.
| Cecha | Object Adapter | Class Adapter |
|---|---|---|
| Mechanizm | Kompozycja (zawiera Adaptee) | Dziedziczenie (extends Adaptee) |
| Elastyczność | Większa — Adaptee można zmieniać w runtime | Mniejsza — Adaptee jest stały w czasie kompilacji |
| Powiazanie | Słabe (przez interfejs) | Silne (przez dziedziczenie) |
| Adaptacja podklas | Tak — Adaptee może być dowolną podklasą | Nie — tylko konkretna superklasa |
| Dostępność | Java, Kotlin, Swift, C++, C# | Tylko języki z wielodziedziczeniem (C++) |
W programowaniu mobilnym używany jest tylko Object Adapter. RecyclerView.Adapter zawiera odniesienia do danych i LayoutInflater, UITableViewDataSource zawiera tablicę elementów. Kompozycja czyni kod bardziej elastycznym i testowalnym w porównaniu z dziedziczeniem.
Adapter jest stosowany nie tylko do list. Rozważmy trzy scenariusze z praktyki programowania mobilnego, gdzie wzorzec rozwiązuje problem integracji niekompatybilnych komponentów.
| Scenariusz | Adaptee | Target | Adapter |
|---|---|---|---|
| Integracja starej biblioteki auth | LegacyAuthLib (callback-based) | AuthService (suspend) | LegacyAuthAdapter |
| Adaptacja JSON pod nowy model | GsonParser | JsonParser (Kotlinx Serialization) | GsonAdapter |
| Jednolity interfejs dla push-serwisów | FCM, APNs, Huawei Push | PushService token register | PushServiceAdapter |
Każdy Adapter hermetyzuje logikę przekształcania: GsonAdapter tłumaczy JSON-string na obiekt przez @SerializedName, PushServiceAdapter abstrahuje rejestrację tokena dla różnych platform. Klient (logika biznesowa) pracuje z jednolitym interfejsem i nie zależy od konkretnej implementacji.
Błędy w implementacji Adapter prowadzą do spowolnienia list, wycieków pamięci i błędów aktualizacji danych. Omówimy trzy częste problemy.
onBindViewHolder jest wywoływane dla każdej komórki podczas przewijania. Tworzenie obiektów, parsowanie JSON lub zapytania do bazy danych w tej metodzie prowadzą do zacięć listy (frame drops). Rozwiązanie: wszystkie ciężkie obliczenia wykonywać z wyprzedzeniem, przekazując do adaptera już gotowe dane. Dla obrazów używać bibliotek z buforowaniem — Glide, Coil, Kingfisher.
Wywołanie notifyDataSetChanged() przy każdej aktualizacji przerysowuje całą listę, powodując migotanie i utratę fokusu na polach wprowadzania. Rozwiązanie: używać ListAdapter z DiffUtil w Android lub DiffableDataSource w iOS. Obliczanie różnic zajmuje <1 ms dla list do 1000 elementów i zapewnia płynne animacje.
Adapter żyje tak długo, jak RecyclerView/UITableView. Przechowywanie Bitmap lub dużych tablic w adapterze prowadzi do wycieków pamięci przy zmianie konfiguracji (obrót ekranu, zmiana motywu). Rozwiązanie: adapter powinien przechowywać tylko lekkie modele danych (data class / struct), a ciężkie zasoby ładować przez ViewHolder na żądanie.
Często zadawane pytania
Adapter przekształca interfejs Adaptee na interfejs Target — klient otrzymuje nowy sposób interakcji. Proxy udostępnia ten sam interfejs co oryginalny obiekt, ale dodaje kontrolę dostępu, buforowanie lub leniwą inicjalizację. Proxy nie zmienia interfejsu, Adapter — zmienia.
RecyclerView.Adapter przekształca dane (np. listę User) w ViewHolder, który RecyclerView może wyświetlić. RecyclerView oczekuje ViewHolder, dane mają format List<User> — Adapter dopasowuje jedno do drugiego. Dodatkowo Adapter zarządza cyklem życia ViewHolder przez recycle-pulę, zwiększając wydajność listy.
Object Adapter wykorzystuje kompozycję: zawiera odniesienie do Adaptee i deleguje mu wywołania. Class Adapter wykorzystuje wielodziedziczenie: dziedziczy Adaptee i implementuje Target. W Java, Kotlin i Swift dostępny jest tylko Object Adapter z powodu braku wielodziedziczenia klas. Class Adapter jest możliwy tylko w C++.
Adapter jest używany, gdy trzeba sprawić, by dwie istniejące klasy działały razem, przekształcając interfejs jednej z nich. Facade — gdy trzeba uprościć interakcję ze złożonym podsystemem, udostępniając prosty interfejs. Adapter — dla kompatybilności, Facade — dla uproszczenia.
Tak, za pomocą domknięć. Zamiast interfejsu z jedną metodą można przekazać funkcję. Na przykład w Swift: let adapter = { (data: Data) -> [CellConfig] in /* przekształcanie */ }. Takie podejście nazywa się functional adapter. Jednak dla 2+ metod protokół/interfejs pozostaje preferowany — czyni kontrakt jawnym.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również