Постараюсь рассказать об опыте использования динамической генерации 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.