Skip to content

Dynamic UI Generation in Android Application Using RecyclerView and AdapterDelegates

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 item in the list belongs to the specified class. First, we create an interface that will belong to a specific page of the application and inherit the class DiffEquals

If an element needs to be reused, we create a super interface that will inherit all the interfaces of the pages that should have this element, 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, inheriting 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 interface of the application page

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 only needed if we use super interface
        // to reuse elements, in this example
        // it is not mandatory
        accountNameDelegate as AdapterDelegate<MutableList<AccountAdapterMarker>>
      )
     
    }
  }

  
   // inherit this interface in the fragment, 
   // to handle clicks and other actions
   // and we 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.

Copyright: Roman Kryvolapov