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