Adapter is a structural design pattern that converts the interface of one class into another interface expected by the client. In mobile development, Adapter is most often used in RecyclerView.Adapter for Android and UITableViewDataSource for iOS. According to Google I/O (2024), RecyclerView is used in 95% of Android apps to display lists, and each one requires its own Adapter implementation.
Key Takeaways
Adapter is a structural pattern that solves the problem of interface incompatibility. It wraps one object (Adaptee) into a class (Adapter) with the interface expected by the client (Target). The client works with Target without knowing about the existence of Adaptee. This is a variant of the Wrapper pattern.
// Existing class with incompatible interface
class LegacyAuthApi {
fun loginWithToken(token: String): Map {
return mapOf("status" to "ok", "user_id" to 42)
}
}
// Target interface (what the client expects)
interface AuthService {
suspend fun login(credentials: Credentials): Result
}
// Adapter — transforms LegacyAuthApi into 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 converts the login(credentials) call into loginWithToken(token) for the old API. The client (ViewModel) works through the AuthService interface and does not know whether it is using the latest Firebase Auth or a decade-old legacy API underneath. This allows replacing implementations without changing the client code.
RecyclerView.Adapter is the most common implementation of the Adapter pattern in Android. It converts data (a list of objects) into ViewHolders that RecyclerView displays on screen. With the introduction of ListAdapter (Android Architecture Components), the pattern gained built-in diff calculation support for animating changes.
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 uses ListAdapter with DiffUtil for efficient list redrawing. When the data is updated, DiffCallback calculates the difference between the old and new list, and RecyclerView animates only the changed items — without redrawing the entire list. This is a key advantage over the older ListView, which required manual notifyDataSetChanged().
UITableViewDataSource is both the Adapter pattern and the DataSource pattern simultaneously. It converts model data into table cells. Since iOS 13, Apple introduced DiffableDataSource — a modern replacement that, like ListAdapter in Android, automatically calculates changes and animates updates.
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 version: 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 with DiffableDataSource automatically animates changes when data is updated: items appear, disappear, or move with smooth animations. iOS DiffableDataSource and Android ListAdapter solve the same problem with different APIs but the same logic — diff calculation + automatic animation.
Object Adapter uses composition: the adapter holds a reference to the Adaptee object and delegates calls to it. Class Adapter uses inheritance: the adapter inherits Adaptee and implements the target interface simultaneously. In Java, Swift, and Kotlin, Class Adapter is impossible due to the lack of multiple inheritance — only Object Adapter remains.
| Characteristic | Object Adapter | Class Adapter |
|---|---|---|
| Mechanism | Composition (contains Adaptee) | Inheritance (extends Adaptee) |
| Flexibility | Higher — Adaptee can be changed at runtime | Lower — Adaptee is fixed at compile time |
| Coupling | Loose (via interface) | Tight (via inheritance) |
| Subclass adaptation | Yes — Adaptee can be any subclass | No — only a specific superclass |
| Availability | Java, Kotlin, Swift, C++, C# | Only languages with multiple inheritance (C++) |
In mobile development, only Object Adapter is used. RecyclerView.Adapter holds references to data and LayoutInflater, UITableViewDataSource holds an array of items. Composition makes the code more flexible and testable compared to inheritance.
Adapter is used not only for lists. Let's consider three scenarios from mobile development practice where the pattern solves the problem of integrating incompatible components.
| Scenario | Adaptee | Target | Adapter |
|---|---|---|---|
| Integrating a legacy auth library | LegacyAuthLib (callback-based) | AuthService (suspend) | LegacyAuthAdapter |
| Adapting JSON to a new model | GsonParser | JsonParser (Kotlinx Serialization) | GsonAdapter |
| Unified interface for push services | FCM, APNs, Huawei Push | PushService token register | PushServiceAdapter |
Each Adapter encapsulates conversion logic: GsonAdapter translates a JSON string into an object via @SerializedName, PushServiceAdapter abstracts token registration for different platforms. The client (business logic) works with a unified interface and does not depend on the specific implementation.
Mistakes in Adapter implementation lead to list lag, memory leaks, and bugs with data updates. Let's look at three common problems.
onBindViewHolder is called for each cell during scrolling. Creating objects, parsing JSON, or calling the database in this method leads to frame drops. Solution: perform all heavy computations in advance, passing ready-made data to the adapter. Use caching libraries for images — Glide, Coil, Kingfisher.
Calling notifyDataSetChanged() on every update redraws the entire list, causing flickering and losing focus on inputs. Solution: use ListAdapter with DiffUtil in Android or DiffableDataSource in iOS. Diff calculation takes <1 ms for lists up to 1000 items and ensures smooth animations.
The adapter lives as long as RecyclerView/UITableView. Storing Bitmaps or large arrays in the adapter leads to memory leaks on configuration changes (screen rotation, theme change). Solution: the adapter should store only lightweight data models (data class / struct), and load heavy resources through ViewHolder on demand.
Frequently Asked Questions
Adapter converts the Adaptee interface to the Target interface — the client gets a new way of interaction. Proxy provides the same interface as the original object but adds access control, caching, or lazy initialization. Proxy does not change the interface, Adapter does.
RecyclerView.Adapter converts data (e.g., a list of User) into a ViewHolder that RecyclerView can display. RecyclerView expects a ViewHolder, the data is in List<User> format — the Adapter adapts one to the other. Additionally, the Adapter manages the ViewHolder lifecycle through the recycle pool, improving list performance.
Object Adapter uses composition: it holds a reference to Adaptee and delegates calls to it. Class Adapter uses multiple inheritance: it inherits Adaptee and implements Target. In Java, Kotlin, and Swift, only Object Adapter is available due to the lack of multiple class inheritance. Class Adapter is only possible in C++.
Adapter is used when you need to make two existing classes work together by converting the interface of one of them. Facade is used when you need to simplify interaction with a complex subsystem by providing a simple interface. Adapter is for compatibility, Facade is for simplification.
Yes, using closures. Instead of an interface with one method, you can pass a function. For example, in Swift: let adapter = { (data: Data) -> [CellConfig] in /* conversion */ }. This approach is called a functional adapter. However, for 2+ methods, a protocol/interface remains preferable — it makes the contract explicit.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also