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

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

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

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

Комментарии (0)