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 или стари API стар десет година. Ово омогућава замену имплементација без промене кода клијента.
RecyclerView.Adapter — најраспрострањенија имплементација обрасца Adapter у Android-у. Он претвара податке (листу објеката) у ViewHolder-е које RecyclerView приказује на екрану. Са појавом ListAdapter-а (Android Architecture Components) образац је добио уграђену подршку за израчунавање разлика за анимацију промена.
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-јима, али по истој логици — израчунавање разлика + аутоматска анимација.
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-у. Израчунавање разлика траје <1 ms за листе до 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-а кроз рецикл-пул, повећавајући перформансе листе.
Object Adapter користи композицију: садржи референцу на Adaptee и делегира му позиве. Class Adapter користи вишеструко наслеђивање: наслеђује Adaptee и имплементира Target. У Java, Kotlin и Swift доступан је само Object Adapter због недостатка вишеструког наслеђивања класа. Class Adapter је могућ само у C++.
Adapter се користи када треба натерати две постојеће класе да раде заједно, претварањем интерфејса једне од њих. Facade — када треба поједноставити интеракцију са сложеним подсистемом, пружајући једноставан интерфејс. Adapter — за компатибилност, Facade — за поједностављење.
Да, помоћу затварања (closures). Уместо интерфејса са једним методом, може се проследити функција. На пример, у Swift-у: let adapter = { (data: Data) -> [CellConfig] in /* претварање */ }. Овај приступ се назива functional adapter. Међутим, за 2+ метода протокол/интерфејс остаје пожељнији — чини уговор експлицитним.
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође