Динамично генериране на UI в Android приложение с помощта на RecyclerView и AdapterDelegates
Ще се опитам да разкажа за опита си с използването на динамично генериране на UI в Android приложение с помощта на RecyclerView и библиотеката
https://github.com/sockeqwe/AdapterDelegates
Има различни начини за работа с UI в Android приложение – например можете да направите UI статичен или да го генерирате с помощта на Jetpack Compose, а можете да изберете и някакъв междинен вариант, при който елементите ще се използват повторно на ниво код, а за показването им ще се използва RecyclerView. Първо добавяме библиотеката по съответната инструкция от нейната страница.
Създаване на интерфейси и класове
В библиотеката има клас AdapterDelegate, в който има метод 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 accountNameClickListener: ((accountName: AccountName) -> Unit)? = null
override fun isForViewType(items: MutableList<AccountAdapterMarker>, 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{ accountNameClickListener?.invoke(accountName) } } }
}Създаване на адаптер
съответно правим такъв делегат за всеки елемент, който трябва да присъства на страницата. След това правим адаптер, в който добавяме делегатите на тези елементи, също с генерик интерфейса на страницата на приложението
class AccountAdapter ( private val accountNameDelegate: AccountNameDelegate,) : AsyncListDifferDelegationAdapter<AccountAdapterMarker>(DefaultDiffUtilCallback()) {
var accountClickListener: AccountClickListener? = null set(value) { field = value accountNameDelegate.accountNameClickListener = { 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.