Последняя статья об упрощении recycler view

Kate

Administrator
Команда форума
За всё время существования Recycler View регулярно выходят статьи, рассказывающие о новых путях упрощения работы с этим элементом. Они появляются так часто, что порой удивляешься тому, откуда у людей столько фантазии, чтоб придумывать всё новые и новые способы работы со списками. А потом открываешь статью и удивляешься второй раз, ведь способ-то вовсе и не новый, а что-то подобное уже было в нескольких предыдущих статьях. Так к чему это я?

Не ругайтесь сильно, если эта статья покажется вам знакомой или очевидной. Мне она тоже кажется таковой, но вспомним, что о списках сказано так много, но подобного я не встречал. Либо просто не смог осилить все, чтоб убедиться в обратном. В таком случае можете поругаться. Но сначала прошу под кат.

Тут я хочу поделиться несколькими приёмами, которые позволят вам упростить работу с recycler view, помогут переиспользовать элементы списка и в большинстве случаев полностью забыть про создание адаптеров и вьюхолдеров.

Для моего удобства объяснения и вашего понимания предлагаю все последующие приёмы разбирать на примере просто элемента HeaderView. Простой не по той причине, что на сложных примерах статья не работает. Так мы сможем сконцентрироваться на сути, а не на попытках разобраться в коде.

Layout
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/headerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:textAppearance="?textAppearanceHeadline1"
tools:text="@tools:sample/lorem" />
</FrameLayout>
Модель с данными
data class HeaderViewItem(
val title: String,
)
HeaderAdapter
class HeaderAdapter(private var entities: List<HeaderViewItem>) : RecyclerView.Adapter<HeaderViewHolder>() {

override fun getItemCount() = entities.size

override fun getItemViewType(position: Int) = 0

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.view_header, parent, false)
return HeaderViewHolder(itemView)
}

override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
holder.bind(entities[position])
}
}
HeaderViewHolder
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

private val headerView = itemView.findViewById<TextView>(com.usacheow.coreui.R.id.headerView)

fun bind(model: HeaderViewItem) {
headerView.text = model.title
}
}

Уберите код из view holder​

Да, первым делом предлагаю вам убрать всю логику заполнения view из HeaderViewHolder. Но где в таком случае его писать? В каких-то статьях вам скажут вынести его в метод onBindViewHolder(...). В некоторых будут убеждать, что ему самое место в лямбде, которая передаётся в адаптер при его создании. Я же предложу создать класс HeaderView, описывающий непосредственно наш элемент и логику его заполнения.

Выглядеть он может как-то так
class HeaderView @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = 0,
) : FrameLayout(context, attributeSet, defStyleAttr) {

private val binding by lazy { ViewHeaderBinding.bind(this) }

fun populate(model: HeaderViewItem) {
binding.headerView.text = model.title
}
}
А HeaderViewHolder становится таким
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

fun bind(model: HeaderViewItem) {
(itemView as? HeaderView)?.populate(model)
}
}
Дополнительно нам потребуется доработать layout, заменив корневой FrameLayout на HeaderView.

Вот так
<com.android.example.HeaderView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/headerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:textAppearance="?textAppearanceHeadline1"
tools:text="@tools:sample/lorem" />
</com.android.example.HeaderView>

Используйте layoutId вместо viewType​

Думаю, что в большинстве ваших адаптеров есть метод getItemViewType(...), который возвращает 0 или константы HEADER_ITEM, FOOTER_ITEM и тд. А по ним вы выбираете вьюхолдер для текущего элемента. Но зачем вводить дополнительные значения, если у нас уже есть константа, однозначно определяющая текущий элемент? Для этой доработки нам потребуется ввести 2 новые сущности:

  • ViewState - абстрактный класс модели данных, который содержит ссылку на layoutId
abstract class ViewState(
@LayoutRes val layoutId: Int,
)
  • Populatable - интерфейс для view с методом принимающим модель данных и заполняющим по ней текущую view
interface Populatable<MODEL> {

fun populate(model: MODEL)
}
Теперь наследуем HeaderViewItem от ViewState, а HeaderView — от Populatable.

HeaderViewItem
data class HeaderViewItem(
val title: String,
) : ViewState(R.layout.view_header)
HeaderView
class HeaderView @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = 0,
) : FrameLayout(context, attributeSet, defStyleAttr), Populatable<HeaderViewItem> {

private val binding by lazy { ViewHeaderBinding.bind(this) }

override fun populate(model: HeaderViewItem) {
binding.headerView.text = model.title
}
}
Внесём (забегая вперёд) последние доработки в HeaderViewHolder.

Он теперь принимает на вход MODEL, потому что ему не требуется знать конкретный тип объекта, нужно лишь передать объект дальше. Это делает его универсальным вьюхолдером, который теперь можно назвать SimpleViewHolder.

HeaderViewHolder -> SimpleViewHolder
class SimpleViewHolder<MODEL>(itemView: View) : RecyclerView.ViewHolder(itemView), Populatable<MODEL> {

override fun populate(model: MODEL) {
(itemView as? Populatable<MODEL>)?.populate(model)
}
}
И немного доработаем наш HeaderAdapter.

Обратите внимание на строчку 8: мы можем передать viewType в метод inflate(...), потому что теперь метод getItemViewType(...) возвращает layoutId, в котором как раз и лежит ссылка на наш layout.

Также мы заменили тип списка enitites на ViewState, потому что адаптеру не нужно знать конкретный тип принимаемого объект: достаточно лишь получить ссылку на layout, заинфлейтить его и передать во вьюхолдер. Теперь, если мы захотим отобразить новый тип заголовка, нам достаточно создать аналогичную модель данных и передать её в этот же список.

Более того, мы можем передать сюда любой элемент, созданный по аналогии с HeaderView, и он отобразится на экране. Заметили, как ловко мы научились отображать списки из разных элементов? Думаю, теперь можно переименовать HeaderAdapter в SimpleAdapter.

HeaderAdapter -> SimpleAdapter
class SimpleAdapter(private var entities: List<ViewState>) : RecyclerView.Adapter<SimpleViewHolder<ViewState>>() {

override fun getItemCount() = entities.size

override fun getItemViewType(position: Int) = entities[position].layoutId

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleViewHolder<ViewState> {
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return SimpleViewHolder(itemView)
}

override fun onBindViewHolder(holder: SimpleViewHolder<ViewState>, position: Int) {
holder.populate(entities[position])
}
}
В итоге получаем SimpleAdapter и SimpleViewHolder, которые можно переиспользовать для большого количества случаев, когда вам нужно отобразить список из разных элементов разной сложности.

На этом на сегодня всё. В дальнейшем могу рассказать, как на основе этого подхода реализовать список с поведением radio group и checkbox (то есть с выбором одного/нескольких элементов), так что пишите комментарии, ставьте лайки, можно и дизлайки. Всего доброго!

 
Сверху