Перейти к содержимому

Динамическая генерация 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 : DiffEquals
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 с дженериком интерфейса

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.

Copyright: Roman Kryvolapov