Динамічна генерація UI в Android застосунку за допомогою RecyclerView та AdapterDelegates
Спробую розповісти про досвід використання динамічної генерації UI в Android застосунку за допомогою RecyclerView та бібліотеки
https://github.com/sockeqwe/AdapterDelegates
Є різні способи роботи з UI в Android застосунку — наприклад, ви можете зробити UI статичним, або згенерувати за допомогою Jetpack Compose, а можна обрати і певний проміжний варіант, де елементи будуть перевикористовуваними на рівні коду, а для їх відображення використовуватиметься RecyclerView. Спочатку додаємо бібліотеку за відповідною інструкцією з її сторінки.
Створення інтерфейсів та класів
У бібліотеці є клас AdapterDelegat, у якому є метод isForViewType, він повертає, чи належить елемент у списку до вказаного класу. Спочатку створюємо інтерфейс, який належатиме до певної сторінки застосунку та успадковуватиме клас DiffEquals
Якщо елемент необхідно перевикористати, створюємо super інтерфейс, який успадковуватиме всі інтерфейси сторінок, на яких має бути цей елемент, а в делегаті як дженерик використовуємо цей super інтерфейс, а не інтерфейс сторінки застосунку
Для кожного унікального елемента у списку створюємо клас, де зберігатимуться його дані, і буде певна логіка для порівняння, аналогічна DiffUtil.ItemCallback
interface DiffEquals {
fun isItemSame(other: Any?): Boolean
fun isContentSame(other: Any?): Boolean
}fun interface DeepCopy<T> {
fun deepCopy(): T
}@Suppress("UNCHECKED_CAST")inline fun <reified T> T.equalTo(other: Any?, vararg properties: T.() -> Any?): Boolean { if (other !is T) return false properties.forEach { val currentValue = it.invoke(this) val otherValue = it.invoke(other) if (currentValue is List<*>) { if (otherValue !is List<*> || !currentValue.isListContentEqualsOrSame(otherValue)) { return false } } else if (it.invoke(this) != it.invoke(other)) { return false } } return true}
/** * Compare two lists by their content. * The entity of this list must have a DiffEquals implementation. * @return true when same, false otherwise */fun <T : DiffEquals> List<T>.isListContentSame(other: List<T>): Boolean { if (size != other.size) return false withIndex().forEach { if (!it.value.isContentSame(other[it.index])) { return false } } return true}
/** * Compare two lists by their content. * This method uses a default equal operation to compare list items * @return true when same, false otherwise */fun List<*>.isListContentEquals(other: List<*>): Boolean { if (size != other.size) return false withIndex().forEach { if (it.value != other[it.index]) { return false } } return true}
/** * Compare two lists by their content. * This method uses a default equal operation to compare list items * or the diff equals mechanic if the list item is implements [DiffEquals] * @return true when same, false otherwise */fun List<*>.isListContentEqualsOrSame(other: List<*>): Boolean { if (size != other.size) return false withIndex().forEach { if (it.value is DiffEquals && other[it.index] is DiffEquals) { if (!(it.value as DiffEquals).isContentSame(other[it.index])) { return false } } else if (it.value != other[it.index]) { return false } } return true}
/** * Makes a deep copy of the list. The items in the list should implement * [DeepCopy] interface. */fun <T : DeepCopy<T>> List<T>.deepCopy(): List<T> { val oldList = this return mutableListOf<T>().apply { oldList.forEach { add(it.deepCopy()) } }}
fun <T> List<T>.moveItemToFirstPosition(predicate: (T) -> Boolean): List<T> { for (element in this.withIndex()) { if (predicate(element.value)) { return this.toMutableList().apply { removeAt(element.index) add(0, element.value) }.toList() } } return this}interface AccountAdapterMarker : DiffEqualsdata class AccountName( val accountName: String,) : AccountAdapterMarker {
override fun isItemSame(other: Any?): Boolean { return equalTo( other, { accountName }, ) }
override fun isContentSame(other: Any?): Boolean { return equalTo( other, { accountName }, ) }
}Створення делегата
далі створюємо делегат для цього класу, що успадковується від класу бібліотеки AdapterDelegate з дженериком інтерфейсу
class AccountNameDelegate() : AdapterDelegate<MutableList<AccountAdapterMarker>>() {
var accontNameClickListener: ((accountName: AccountName) -> Unit)? = null
override fun isForViewType(items: MutableList<CommonTitleAdapterMarker>, position: Int): Boolean { return items[position] is AccountName }
override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { return ViewHolder(parent.inflateBinding(ListItemAccountNameBinding::inflate)) }
override fun onBindViewHolder( items: MutableList<AccountAdapterMarker>, position: Int, holder: RecyclerView.ViewHolder, payloads: MutableList<Any> ) { (holder as ViewHolder).bind(items[position] as AccountName) }
private inner class ViewHolder( private val binding: ListItemAccountNameBinding, ) : RecyclerView.ViewHolder(binding.root) {
fun bind(accountName: AccountName) { binding.tvAccountName.text = accountName.accountName binding.tvAccountName.setOnClickListener{ accontNameClickListener?.invoke(accountName) } } }
}Створення адаптера
відповідно, робимо такий делегат для кожного елемента, який має бути присутнім на сторінці. Далі робимо адаптер, до якого додаємо делегати цих елементів, також з дженериком інтерфейсу сторінки застосунку
class AccountAdapter ( private val accountNameDelegate: AccountNameDelegate,) : AsyncListDifferDelegationAdapter<AccountAdapterMarker>(DefaultDiffUtilCallback()) {
var accountClickListener: AccountClickListener? = null set(value) { field = value accountNameDelegate.accontNameClickListener = { field?.onAccountNameClicked(it) } }
init { items = mutableListOf() @Suppress("UNCHECKED_CAST") delegatesManager.apply { addDelegate( // каст потрібен, тільки якщо використовуємо super інтерфейс // для перевикористання елементів, а в цьому прикладі // він не обов'язковий accountNameDelegate as AdapterDelegate<MutableList<AccountAdapterMarker>> )
} }
// успадковуємо цей інтерфейс у фрагменті, // щоб обробляти натискання та інші дії // і прописуємо у фрагменті // accountAdapter.accountClickListener = this interface AccountClickListener{ fun onAccountNameClicked(accountName: AccountName) }
}Використання
ну а далі потрібно у ViewModel або спеціальному мапері створити список з екземплярами класів, що успадковують AccountAdapterMarker, і викликати в адаптера метод setItems, а адаптер уже розбереться, інстанси якого класу в ньому знаходяться, і додасть ці елементи до RecyclerView.