Adapter — je strukturální návrhový vzor, který převádí rozhraní jedné třídy na jiné rozhraní očekávané klientem. V mobilním vývoji se Adapter nejčastěji používá v RecyclerView.Adapter pro Android a UITableViewDataSource pro iOS. Podle údajů Google I/O (2024) se RecyclerView používá v 95% Android aplikací pro zobrazení seznamů a každá vyžaduje vlastní implementaci Adapteru.
Hlavní body
Adapter — strukturální vzor, který řeší problém nekompatibility rozhraní. Zabalí jeden objekt (Adaptee) do třídy (Adapter) s rozhraním očekávaným klientem (Target). Klient pracuje s Target, aniž by věděl o existenci Adaptee. Toto je varianta vzoru Wrapper.
// Existující třída s nekompatibilním rozhraním
class LegacyAuthApi {
fun loginWithToken(token: String): Map {
return mapOf("status" to "ok", "user_id" to 42)
}
}
// Cílové rozhraní (co klient očekává)
interface AuthService {
suspend fun login(credentials: Credentials): Result
}
// Adapter — převádí LegacyAuthApi na 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 převádí volání login(credentials) na loginWithToken(token) pro staré API. Klient (ViewModel) pracuje přes rozhraní AuthService a neví, zda je pod ním nejnovější Firebase Auth nebo deset let staré API. To umožňuje výměnu implementací bez změny klientského kódu.
RecyclerView.Adapter — nejrozšířenější implementace vzoru Adapter v Android. Převádí data (seznam objektů) na ViewHoldery, které RecyclerView zobrazuje na obrazovce. S příchodem ListAdapter (Android Architecture Components) získal vzor vestavěnou podporu výpočtu rozdílů pro animaci změn.
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 používá ListAdapter s DiffUtil pro efektivní překreslování seznamu. Když jsou data aktualizována, DiffCallback vypočítá rozdíl mezi starým a novým seznamem a RecyclerView animuje pouze změněné prvky — bez překreslení celého seznamu. To je klíčová výhoda oproti staršímu ListView, který vyžadoval ruční notifyDataSetChanged().
UITableViewDataSource — je současně vzor Adapter i DataSource. Převádí data modelu na buňky tabulky. Od iOS 13 Apple představil DiffableDataSource — moderní náhradu, která podobně jako ListAdapter v Android automaticky vypočítává změny a animuje aktualizace.
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
}
}
// Moderní verze: 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 s DiffableDataSource automaticky animuje změny při aktualizaci dat: prvky se objevují, mizí nebo přesouvají s plynulými animacemi. iOS DiffableDataSource a Android ListAdapter řeší stejný úkol různými API, ale stejnou logikou — výpočet rozdílů + automatická animace.
Object Adapter používá kompozici: adaptér obsahuje odkaz na objekt Adaptee a deleguje mu volání. Class Adapter používá dědičnost: adaptér dědí Adaptee a současně implementuje cílové rozhraní. V Java, Swift a Kotlin je Class Adapter nemožný kvůli chybějící vícenásobné dědičnosti — zbývá pouze Object Adapter.
| Vlastnost | Object Adapter | Class Adapter |
|---|---|---|
| Mechanismus | Kompozice (obsahuje Adaptee) | Dědičnost (extends Adaptee) |
| Flexibilita | Vyšší — Adaptee lze měnit za běhu | Nižší — Adaptee je fixní při kompilaci |
| Provázanost | Slabá (přes rozhraní) | Silná (přes dědičnost) |
| Adaptace podtříd | Ano — Adaptee může být libovolná podtřída | Ne — pouze konkrétní nadtřída |
| Dostupnost | Java, Kotlin, Swift, C++, C# | Pouze jazyky s vícenásobnou dědičností (C++) |
V mobilním vývoji se používá pouze Object Adapter. RecyclerView.Adapter obsahuje odkazy na data a LayoutInflater, UITableViewDataSource obsahuje pole prvků. Kompozice činí kód flexibilnějším a testovatelnějším ve srovnání s dědičností.
Adapter se aplikuje nejen pro seznamy. Podívejme se na tři scénáře z praxe mobilního vývoje, kde vzor řeší problém integrace nekompatibilních komponent.
| Scénář | Adaptee | Target | Adapter |
|---|---|---|---|
| Integrace staré auth knihovny | LegacyAuthLib (callback-based) | AuthService (suspend) | LegacyAuthAdapter |
| Adaptace JSON pod nový model | GsonParser | JsonParser (Kotlinx Serialization) | GsonAdapter |
| Jednotné rozhraní pro push služby | FCM, APNs, Huawei Push | PushService token register | PushServiceAdapter |
Každý Adapter zapouzdřuje logiku převodu: GsonAdapter překládá JSON řetězec na objekt přes @SerializedName, PushServiceAdapter abstrahuje registraci tokenu pro různé platformy. Klient (business logika) pracuje s jednotným rozhraním a nezávisí na konkrétní implementaci.
Chyby v implementaci Adapteru vedou ke zpomalení seznamů, únikům paměti a chybám při aktualizaci dat. Rozebereme tři časté problémy.
onBindViewHolder je volán pro každou buňku při scrollování. Vytváření objektů, parsování JSON nebo dotazy do databáze v této metodě vedou k trhání seznamu (frame drops). Řešení: všechny těžké výpočty provádět předem, předávat adaptéru již hotová data. Pro obrázky používat knihovny s cachingem — Glide, Coil, Kingfisher.
Volání notifyDataSetChanged() při každé aktualizaci překreslí celý seznam, způsobuje blikání a ztrátu fokusu na vstupních polích. Řešení: používat ListAdapter s DiffUtil v Android nebo DiffableDataSource v iOS. Výpočet rozdílů trvá <1 ms pro seznamy do 1000 prvků a zajišťuje plynulé animace.
Adaptér žije stejně dlouho jako RecyclerView/UITableView. Ukládání Bitmap nebo velkých polí v adaptéru vede k únikům paměti při změně konfigurace (otočení obrazovky, změna motivu). Řešení: adaptér by měl ukládat pouze lehké datové modely (data class / struct) a těžké zdroje načítat přes ViewHolder na vyžádání.
Často kladené otázky
Adapter převádí rozhraní Adaptee na rozhraní Target — klient získává nový způsob interakce. Proxy poskytuje stejné rozhraní jako původní objekt, ale přidává řízení přístupu, caching nebo línou inicializaci. Proxy nemění rozhraní, Adapter — mění.
RecyclerView.Adapter převádí data (např. seznam User) na ViewHolder, který RecyclerView může zobrazit. RecyclerView očekává ViewHolder, data jsou ve formátu List<User> — Adapter přizpůsobuje jedno druhému. Navíc Adapter spravuje životní cyklus ViewHolder přes recyklační pool, čímž zvyšuje výkon seznamu.
Object Adapter používá kompozici: obsahuje odkaz na Adaptee a deleguje mu volání. Class Adapter používá vícenásobnou dědičnost: dědí Adaptee a implementuje Target. V Java, Kotlin a Swift je dostupný pouze Object Adapter kvůli chybějící vícenásobné dědičnosti tříd. Class Adapter je možný pouze v C++.
Adapter se používá, když dvě existující třídy potřebují spolupracovat převedením rozhraní jedné z nich. Facade — když je potřeba zjednodušit interakci se složitým subsystémem poskytnutím jednoduchého rozhraní. Adapter — pro kompatibilitu, Facade — pro zjednodušení.
Ano, pomocí uzávěrů (closures). Místo rozhraní s jednou metodou lze předat funkci. Například ve Swift: let adapter = { (data: Data) -> [CellConfig] in /* převod */ }. Tento přístup se nazývá functional adapter. Pro 2+ metod však protokol/rozhraní zůstává preferovaný — činí kontrakt explicitním.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také