В последнее время я стал реже использовать 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
без лишних заморочек, крашей и багов, поэтому моя статья скорее всего носит ознакомительный характер, ну и в какой-то мере экспериментальный.
В заключении скажу, что я рад любым идеям, даже самым необычным и странным, поэтому не стесняйтесь, пишите :)
Всем хорошего кода!
А, ну и ссылочка на репо на всякий случай.