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