I will try to tell about the experience of using dynamic UI generation in an Android application using RecyclerView and the library
https://github.com/sockeqwe/AdapterDelegates
There are different ways to work with UI in an Android application – for example, you can make the UI static, or generate it using Jetpack Compose, or you can choose some intermediate option, where the elements will be reused at the code level, and RecyclerView will be used to display them. First, add the library according to the corresponding instructions from its page.
The library has a class AdapterDelegat, which has a method isForViewType, it returns whether the element in the list belongs to the specified class. First, we create an interface that will be specific to a specific application page and inherit the DiffEquals class
If an element needs to be reused, we create a super interface that will inherit all interfaces of the pages where this element should be, and in the delegate we use this super interface as a generic, not the application page interface
For each unique element in the list, we create a class where its data will be stored, and there will be some logic for comparison, similar to 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 }, ) } }
Next, we create a delegate for this class, inherited from the AdapterDelegate library class with a generic interface
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) } } } }
accordingly, we make such a delegate for each element that should be present on the page. Next, we make an adapter to which we add delegates of these elements, also with a generic of the application page interface
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( // cast is needed only if we use super interface // for reusing elements, in this example // it is not required accountNameDelegate as AdapterDelegate<MutableList<AccountAdapterMarker>> ) } } // inherit this interface in the fragment, // to handle clicks and other actions // and write it in the fragment // accountAdapter.accountClickListener = this interface AccountClickListener { fun onAccountNameClicked(accountName: AccountName) } }
Well, then you need to create a list in the ViewModel or a special mapper with instances of classes that inherit AccountAdapterMarker, and call the setItems method on the adapter, and the adapter will already figure out which class instances are in it, and add these elements to the RecyclerView.