Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров1.6K

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

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии0

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань