Roman Kryvolapov Engineering Blog

Динамично генериране на 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

Kotlin
interface DiffEquals {
fun isItemSame(other: Any?): Boolean
fun isContentSame(other: Any?): Boolean
}
Kotlin
fun interface DeepCopy<T> {
fun deepCopy(): T
}
Kotlin
@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
}
Kotlin
interface AccountAdapterMarker : DiffEquals
Kotlin
data 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 на библиотеката с генерик интерфейса

Kotlin
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) }
}
}
}

Създаване на адаптер

съответно правим такъв делегат за всеки елемент, който трябва да присъства на страницата. След това правим адаптер, в който добавяме делегатите на тези елементи, също с генерик интерфейса на страницата на приложението

Kotlin
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.

Copyright: Roman Kryvolapov