Мое надуманное решение «Как создать RecyclerView Adapter»

от автора

В последнее время я стал реже использовать xml разметку, чтобы сверстать экранчик для Activity или Fragment'а.

В основном я пишу UI кодом и мне это очень сильно нравится 🙂

И я наткнулся на проблемку «шаблонное создание адаптера для RecyclerView«.

Ну что ж, я покажу как я пришел к моему решению, но сразу сделаю предупреждение, возможно мое решение может быть плохим или в нем есть скрытая ошибка, которая неподвластна моему разуму.

Поэтому прошу вас без «сырой критике» в плане: «что за чушь ты написал» или «говнокод».

Замечание: Приведенное здесь решение используется только там, где UI пишется кодом без, еще раз без применения xml разметки.

Ну что ж, налевайте себе кофе, приготовьте печеньки и погнали!

Шаг 1: CoreAdapter c Generic-типом

Первым делом я создал обычный RecyclerView адаптер, проанализировал его и задал себе вопрос: как я могу абстрагировать создание вьюшки и ее binding от адаптера?

class CoreViewHolder(view: View) : RecyclerView.ViewHolder(view) {}  class CoreAdapter(     private val items: List<String> ) : RecyclerView.Adapter<CoreViewHolder>() {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : CoreViewHolder {              }     override fun onBindViewHolder(holder: CoreViewHolder, position: Int) {                                                    }     override fun getItemCount() = items.size      }

Если абстрагировать, то нужно абстрагироваться и от типа элемента списка, поэтому делаем наш адаптер обобщенным:

class CoreViewHolder<T>(view: View) : RecyclerView.ViewHolder(view) {}  class CoreAdapter<T>(     private val items: List<T> ) : RecyclerView.Adapter<CoreViewHolder>() {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : CoreViewHolder<T> {      }     override fun onBindViewHolder(holder: CoreViewHolder<T>, position: Int) {      }     override fun getItemCount() = items.size  }

Что ж, двигаемся дальше.

Абстрактный класс ViewHolderContainer и интерфейс BindListener

Здесь мне пришлось повозиться, ведь вызовы onCreateViewHolder и onBindViewHolder происходят отдельно друг от друга.

Сначала я создал абстрактный класс ViewHolderContainer, который инкапсулирует в себе создание вьюшки:

abstract class ViewHolderContainer<T> {      abstract fun view(ctx: Context) : View      fun holder(parent: ViewGroup) : CoreViewHolder<T> {         val view = view(parent.context)         return CoreViewHolder(view)     }  }

Окей, создание вьюшки будет происходить в контексте реализации нашего абстрактного класса, поэтому при переопределении метода view мы будет иметь доступ к другим методам ViewHolderContainer'а.

Добавим в конструктор нашего адаптера новый параметр viewHolderContainer и допишем метод onCreateViewHolder:

class CoreAdapter<T>(     private val items: List<T>,     private val viewHolderContainer: ViewHolderContainer<T> ) : RecyclerView.Adapter<CoreViewHolder<T>>() {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : CoreViewHolder<T> {         return viewHolderContainer.holder(parent)     }     override fun onBindViewHolder(holder: CoreViewHolder<T>, position: Int) {                                   }     override fun getItemCount() = items.size }

Теперь нам нужен интерфейс, метод которого будет вызываться, когда в адаптере вызывается onBindViewHolder, я назвал такой интерфейс BindListener:

fun interface BindListener<T> {     fun onBind(pos: Int, item: T) }

Далее мы должны передать этот интерфейс нашему CoreViewHolder'у и не забудем добавить метод bind:

class CoreViewHolder<T>(view: View, private val listener: BindListener<T>) : RecyclerView.ViewHolder(view) {     fun bind(position: Int, item: T) {         listener.onBind(position, item)     } }

Вроде бы здесь все очевидно, в методе адаптера onBindViewHolder будет вызываться наш метод bind, определенный ранее в CoreViewHolder'е:

class CoreAdapter<T>(     private val items: List<T>,     private val viewHolderContainer: ViewHolderContainer<T> ) : RecyclerView.Adapter<CoreViewHolder<T>>() {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : CoreViewHolder<T> {         return viewHolderContainer.holder(parent)     }     override fun onBindViewHolder(holder: CoreViewHolder<T>, position: Int) {         holder.bind(position, items[position])     }     override fun getItemCount() = items.size }

А в методе bind мы дергаем наш интерфейсик, реализация которого передается CoreViewHolder в конструкторе!

Вернемся теперь к нашему абстрактному классу:

abstract class ViewHolderContainer<T> {      abstract fun view(ctx: Context) : View      fun holder(parent: ViewGroup) : CoreViewHolder<T> {         val view = view(parent.context)         return CoreViewHolder(view)     }  }

Здесь нужна реализация BindListener'а.

Мы ведь прекрасно понимаем зачем нам нужен интерфейс BindListener? Он нужен нам, чтобы связать нашу вьюшку с элементом списка.

Так, значит в методе view мы будем иметь доступ к методам ViewHolderContainer'а.

Ага, значит можно сделать так:

abstract class ViewHolderContainer<T> {      abstract fun view(ctx: Context) : View          private var listener: BindListener<T> = BindListener { _, _ -> }      fun onBind(listener: BindListener<T>) {         this.listener = listener     }      fun holder(parent: ViewGroup) : CoreViewHolder<T> {         val view = view(parent.context)         return CoreViewHolder(view, listener)     }  }

Теперь мы можем вызвать onBind, когда создаем вьюшку для элемента списка и получить его, когда в адаптере будет вызван onBindViewHolder

Классно! Но это еще не все (

Kotlin extensions для создания магии!

Давайте создадим вот такой Kotlin extension для RecyclerView:

fun <T> RecyclerView.adapter(items: List<T>, viewHolderContainer: ViewHolderContainer<T>) {     this.adapter = CoreAdapter(items, viewHolderContainer)                                                                     }

Ну что ж, протестим всю эту конструкцию на примере простого списка персонажей из мультисериала My Little Pony:

setContentView(list { vertical()      adapter(     listOf(       "Twilight Sparkle",       "Pinky Pie",       "Fluttershy",       "Rarity",       "Rainbow Dash",       "Apple Jack",       "Starlight Glimmer"     ),     object: ViewHolderContainer<String>() {       override fun view(ctx: Context): View {         return text {           fontSize(18f)           colorRes(R.color.black)           padding(dp(24))           layoutParams(recyclerLayoutParams().matchWidth().wrapHeight().build())           onBind { _, ponyName ->             text(ponyName)           }         }       }     }   ) })

Вуаля!

Здесь помимо ранее написанного нами adapter extension’а есть еще десяток простых Kotlin extensions для создания UI кодом (см. здесь)

Я немного еще заморочился и сделал вот такой страшный Kotlin extension:

fun <T> RecyclerView.adapter(items: List<T>, view: (listenItem: (bindListener: BindListener<T>) -> Unit) -> View) {     this.adapter = CoreAdapter(items, object: ViewHolderContainer<T>() {                    override fun view(ctx: Context): View {             return view(::onBind)         }     }) }

Теперь мы можем сделать вот так:

setContentView(list {   vertical()      adapter(     listOf(       "Twilight Sparkle",       "Pinky Pie",       "Fluttershy",       "Rarity",       "Rainbow Dash",       "Apple Jack",       "Starlight Glimmer"     ),   ) { onBind ->     text {       fontSize(18f)       colorRes(R.color.black)       padding(dp(24))       layoutParams(recyclerLayoutParams().matchWidth().wrapHeight().build())       onBind { _, ponyName ->         text(ponyName)       }     }   } })

Результат:

Добавление DiffUtil.ItemCallback’а

Давайте на примере нашего простого адаптера сделаем еще адаптер, который работает с DiffUtil.ItemCallback'ом:

class CoreAdapter2<T>(     diffUtilItemCallback: DiffUtil.ItemCallback<T>,      private val viewHolderContainer: ViewHolderContainer<T> ) : ListAdapter<T, CoreViewHolder<T>>(diffUtilItemCallback) {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CoreViewHolder<T> {         return viewHolderContainer.holder(parent)     }      override fun onBindViewHolder(holder: CoreViewHolder<T>, position: Int) {         holder.bind(position, getItem(position))     }  }

Добавим для него Kotlin extension’ы:

fun <T> RecyclerView.adapter(diffUtil: DiffUtil.ItemCallback<T>, viewHolderContainer: ViewHolderContainer<T>) : CoreAdapter2<T> {     val adapter = CoreAdapter2(diffUtil, viewHolderContainer)     this.adapter = adapter     return adapter }  fun <T> RecyclerView.adapter(diffUtil: DiffUtil.ItemCallback<T>, view: (listenItem: (bindListener: BindListener<T>) -> Unit) -> View) : CoreAdapter2<T> {     val adapter = CoreAdapter2(diffUtil, object: ViewHolderContainer<T>() {         override fun view(ctx: Context): View {             return view(::onBind)         }     })     this.adapter = adapter     return adapter }

Обратите внимание, здесь мы возвращаем наш адаптер, чтобы затем вызвать широко известный всем submitList:

setContentView(list {   vertical()    val adapter = adapter(object: DiffUtil.ItemCallback<String>() {     override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem     override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem   }) { onBind ->     text {       fontSize(18f)       colorRes(R.color.black)       padding(dp(24))       layoutParams(recyclerLayoutParams().matchWidth().wrapHeight().build())       onBind { _, ponyName ->         text(ponyName)       }     }   }    adapter.submitList(listOf(     "Twilight Sparkle",     "Pinky Pie",     "Fluttershy",     "Rarity",     "Rainbow Dash",     "Apple Jack",     "Starlight Glimmer"   )) })

Результат такой же.

Заключительные соображения

Я хотел бы отметить, что подобные решения сейчас редко используются, так как в Android’е не так много разрабов, у которых есть пристрастие писать разметку кодом без дополнительных библиотек или framework’ов, тем более крупные и сложные проекты.

Я выделил две основные проблемы моего решения:

  • Плохая поддержка (немногие разрабы готовы начать писать разметку кодом)

  • Возможны проблемы с производительностью и неожиданные краши, ведь я писал свое решение буквально на коленке

И еще одно замечание, данное решение имеет неполную функциональность. Например, я не реализовал поддержку нескольких типов элемента списка (viewType).

В наше время полно готовых решений, которые упрощают создание адаптеров для RecyclerView без лишних заморочек, крашей и багов, поэтому моя статья скорее всего носит ознакомительный характер, ну и в какой-то мере экспериментальный.

В заключении скажу, что я рад любым идеям, даже самым необычным и странным, поэтому не стесняйтесь, пишите 🙂

Всем хорошего кода!

А, ну и ссылочка на репо на всякий случай.


ссылка на оригинал статьи https://habr.com/ru/post/652687/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *