Adapter — este un model structural de proiectare care transformă interfața unei clase în altă interfață așteptată de client. În dezvoltarea mobilă, Adapter este cel mai des folosit în RecyclerView.Adapter pentru Android și UITableViewDataSource pentru iOS. Conform datelor Google I/O (2024), RecyclerView este folosit în 95% din aplicațiile Android pentru afișarea listelor, iar fiecare necesită propria implementare Adapter.
Principalele puncte
Adapter — model structural care rezolvă problema incompatibilității interfețelor. Înfășoară un obiect (Adaptee) într-o clasă (Adapter) cu interfața așteptată de client (Target). Clientul lucrează cu Target, fără să știe de existența Adaptee. Aceasta este o variație a modelului Wrapper.
// Clasă existentă cu interfață incompatibilă
class LegacyAuthApi {
fun loginWithToken(token: String): Map {
return mapOf("status" to "ok", "user_id" to 42)
}
}
// Interfață țintă (ce așteaptă clientul)
interface AuthService {
suspend fun login(credentials: Credentials): Result
}
// Adapter — transformă LegacyAuthApi în 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 transformă apelul login(credentials) în loginWithToken(token) pentru API-ul vechi. Clientul (ViewModel) lucrează prin interfața AuthService și nu știe că sub ea se află cel mai nou Firebase Auth sau un API vechi de zece ani. Acest lucru permite înlocuirea implementărilor fără a modifica codul clientului.
RecyclerView.Adapter — cea mai răspândită implementare a modelului Adapter în Android. Transformă datele (o listă de obiecte) în ViewHolder-uri pe care RecyclerView le afișează pe ecran. Odată cu apariția ListAdapter (Android Architecture Components), modelul a primit suport încorporat pentru calculul diferențelor pentru animația modificărilor.
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 folosește ListAdapter cu DiffUtil pentru redarea eficientă a listei. Când datele sunt actualizate, DiffCallback calculează diferența dintre lista veche și cea nouă, iar RecyclerView animează doar elementele modificate — fără a redesena întreaga listă. Acesta este avantajul cheie față de ListView-ul mai vechi, care necesita notifyDataSetChanged() manual.
UITableViewDataSource — este simultan atât modelul Adapter, cât și DataSource. Transformă datele modelului în celule de tabel. Din iOS 13, Apple a introdus DiffableDataSource — un înlocuitor modern care, asemenea ListAdapter în Android, calculează automat modificările și animează actualizările.
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
}
}
// Versiune modernă: 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 cu DiffableDataSource animează automat modificările la actualizarea datelor: elementele apar, dispar sau se deplasează cu animații line. DiffableDataSource din iOS și ListAdapter din Android rezolvă aceeași sarcină cu API-uri diferite, dar cu aceeași logică — calculul diferențelor + animație automată.
Object Adapter folosește compoziția: adapterul conține o referință către obiectul Adaptee și îi delegă apelurile. Class Adapter folosește moștenirea: adapterul moștenește Adaptee și implementează simultan interfața țintă. În Java, Swift și Kotlin, Class Adapter este imposibil din cauza lipsei moștenirii multiple — rămâne doar Object Adapter.
| Caracteristică | Object Adapter | Class Adapter |
|---|---|---|
| Mecanism | Compoziție (conține Adaptee) | Moștenire (extends Adaptee) |
| Flexibilitate | Mai mare — Adaptee poate fi schimbat în runtime | Mai mică — Adaptee este fixat la compilare |
| Cuplare | Slabă (prin interfață) | Puternică (prin moștenire) |
| Adaptarea subclaselor | Da — Adaptee poate fi orice subclasă | Nu — doar superclasa concretă |
| Disponibilitate | Java, Kotlin, Swift, C++, C# | Doar limbaje cu moștenire multiplă (C++) |
În dezvoltarea mobilă se folosește doar Object Adapter. RecyclerView.Adapter conține referințe către date și LayoutInflater, UITableViewDataSource conține un array de elemente. Compoziția face codul mai flexibil și mai testabil comparativ cu moștenirea.
Adapter se aplică nu doar pentru liste. Să examinăm trei scenarii din practica dezvoltării mobile, unde modelul rezolvă problema integrării componentelor incompatibile.
| Scenariu | Adaptee | Target | Adapter |
|---|---|---|---|
| Integrarea unei biblioteci vechi de autentificare | LegacyAuthLib (callback-based) | AuthService (suspend) | LegacyAuthAdapter |
| Adaptarea JSON la un model nou | GsonParser | JsonParser (Kotlinx Serialization) | GsonAdapter |
| Interfață unică pentru servicii push | FCM, APNs, Huawei Push | PushService token register | PushServiceAdapter |
Fiecare Adapter încapsulează logica de transformare: GsonAdapter traduce un string JSON într-un obiect prin @SerializedName, PushServiceAdapter abstrage înregistrarea token-ului pentru diferite platforme. Clientul (logica de business) lucrează cu o interfață unică și nu depinde de o implementare concretă.
Erorile în implementarea Adapter duc la încetinirea listelor, scurgeri de memorie și bug-uri de actualizare a datelor. Să analizăm trei probleme frecvente.
onBindViewHolder este apelat pentru fiecare celulă la derulare. Crearea de obiecte, parsarea JSON sau interogările bazei de date în această metodă duc la tremurul listei (frame drops). Soluție: toate calculele grele să fie făcute dinainte, transmitând datele gata pregătite către adapter. Pentru imagini, folosiți biblioteci cu cache — Glide, Coil, Kingfisher.
Apelarea notifyDataSetChanged() la fiecare actualizare redesenează întreaga listă, provocând pâlpâiri și pierderea focusului pe câmpurile de intrare. Soluție: folosiți ListAdapter cu DiffUtil în Android sau DiffableDataSource în iOS. Calculul diferențelor durează <1 ms pentru liste de până la 1000 de elemente și asigură animații line.
Adapterul trăiește cât RecyclerView/UITableView. Stocarea Bitmap sau a array-urilor mari în adapter duce la scurgeri de memorie la schimbarea configurației (rotirea ecranului, schimbarea temei). Soluție: adapterul ar trebui să stocheze doar modele de date ușoare (data class / struct), iar resursele grele să le încarce prin ViewHolder la cerere.
Întrebări frecvente
Adapter transformă interfața Adaptee în interfața Target — clientul primește un nou mod de interacțiune. Proxy oferă aceeași interfață ca și obiectul original, dar adaugă control de acces, cache sau inițializare lentă. Proxy nu schimbă interfața, Adapter — schimbă.
RecyclerView.Adapter transformă datele (de exemplu, o listă de User) în ViewHolder pe care RecyclerView îl poate afișa. RecyclerView așteaptă ViewHolder, datele sunt în format List<User> — Adapter le adaptează pe unul la celălalt. În plus, Adapter gestionează ciclul de viață al ViewHolder-urilor prin pool-ul de reciclare, îmbunătățind performanța listei.
Object Adapter folosește compoziția: conține o referință către Adaptee și îi delegă apelurile. Class Adapter folosește moștenirea multiplă: moștenește Adaptee și implementează Target. În Java, Kotlin și Swift, doar Object Adapter este disponibil din cauza lipsei moștenirii multiple de clase. Class Adapter este posibil doar în C++.
Adapter este folosit când trebuie să facem două clase existente să funcționeze împreună, transformând interfața uneia dintre ele. Facade — când trebuie să simplificăm interacțiunea cu un subsistem complex, oferind o interfață simplă. Adapter — pentru compatibilitate, Facade — pentru simplificare.
Da, prin închideri (closures). În locul unei interfețe cu o singură metodă, se poate transmite o funcție. De exemplu, în Swift: let adapter = { (data: Data) -> [CellConfig] in /* transformare */ }. Această abordare se numește functional adapter. Însă pentru 2+ metode, protocolul/interfața rămâne preferată — face contractul explicit.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și