В последнее время я стал реже использовать 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/
Добавить комментарий