Prosto: убираем бойлерплейт при работе с RecyclerView

от автора

Для отображения списка данных мы используем RecyclerView (– Спасибо, кэп!). Он много чего умеет из коробки и другие всем известные блаблабла. Но и боли с ним предостаточно. Никто не любит писать один и тот же boilerplate-код. И я вот не особо…

Краткая история сюжета "Немного уменьшить кода":

Для примера создан простой data class Person(): с именем, фамилией, эл. почтой и наличием собаки.

Чтобы вывести на экран список людей, необходимо создать RecyclerView.Adapter и RecyclerView.ViewHolder, большая часть кода которых +- одинаковая.

Если у вас один Adapter использует множество разных ViewHolder-ов, эта история не для вас. В большинстве же, наверное, случаев используется один ViewHolder, который просто отображает однотипные данные.
Для таких случаев я сделал базовый Adapter и ViewHolder, чтобы избавиться от рутины.

Обычная жизнь с RecyclerView.Adapter<RecyclerView.ViewHolder>

Adapter классический. Переопределение базовых методов, ничего нового.

class ClassicAdapter : RecyclerView.Adapter<ClassicHolder>() {      private val viewModel = PersonItemViewModel()      private val data: List<Person>         get() = viewModel.data      fun setData(persons: List<Person>) {         viewModel.data = persons         notifyDataSetChanged()     }      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ClassicHolder =         ClassicHolder.create(parent)      override fun getItemCount(): Int = data.size      override fun onBindViewHolder(holder: ClassicHolder, position: Int) {         holder.bind(viewModel, position)     } }

ViewHolder у меня на MVVM.

class ClassicHolder(private val binding: ItemPersonBinding) : RecyclerView.ViewHolder(binding.root) {     fun bind(viewModel: PersonItemViewModel, position: Int) {         binding.setVariable(BR.viewModel, viewModel)         binding.setVariable(BR.position, position)         binding.executePendingBindings()     }      companion object {         fun create(parent: ViewGroup): ClassicHolder {             val inflater = LayoutInflater.from(parent.context)             val binding: ItemPersonBinding =                 DataBindingUtil.inflate(inflater, R.layout.item_person, parent, false)             return ClassicHolder(binding)         }     } }

В item_person.xml указываем все нужные binding-и: ViewModel с данными и Position — позиция элемента в RecyclerView.

<?xml version="1.0" encoding="utf-8"?> <layout>     <data>         <variable             name="position"             type="Integer" />          <variable             name="viewModel"             type="plus.yeti.prostoadapter.ui.main.PersonItemViewModel" />     </data>      <androidx.constraintlayout.widget.ConstraintLayout>          <TextView             android:text="@{viewModel.getName(position)}"             ... />          <TextView             android:text="@{viewModel.getEmail(position)}"             .../>          <ImageView             app:visible="@{viewModel.hasDog(position)}"              .../>      </androidx.constraintlayout.widget.ConstraintLayout> </layout>

PersonItemViewModel для полноты картины.

class PersonItemViewModel : ProstoViewModel<Person>() {     override var data: List<Person> = emptyList()      fun getName(position: Int) = data[position].lastName + ", " + data[position].firstName     fun getEmail(position: Int) = data[position].email     fun hasDog(position: Int): Boolean = data[position].hasDog }

Создание ProstoAdapter и ProstoHolder

Итак, переводим Adapter и ViewHolder на дженерики, и всё рутинное переносим вовнутрь.

Для начала сделаем базовую ProstoViewModel. Вообще, можно обойтись и без этой ProstoViewModel, но, чтобы в итоге получилось совсем красиво, добавим и её. Это позволит нам устанавливать данные во ViewModel без посредника-адаптера:

abstract class ProstoViewModel<T>: ViewModel() {     abstract var data: List<T> }

ProstoHolder

open class ProstoHolder<TBinding : ViewDataBinding>(val binding: TBinding) : RecyclerView.ViewHolder(binding.root) {     open fun <TData, TViewModel : ProstoViewModel<TData>> bind(viewModel: TViewModel, position: Int) {         binding.setVariable(BR.viewModel, viewModel)         binding.setVariable(BR.position, position)         binding.executePendingBindings()     }      companion object {         fun <TBinding : ViewDataBinding> create(parent: ViewGroup, layoutId: Int): ProstoHolder<TBinding> {             val inflater = LayoutInflater.from(parent.context)             val binding: TBinding = DataBindingUtil.inflate(inflater, layoutId, parent, false)             return ProstoHolder(binding)         }     } }

и, наконец, ProstoAdapter:

abstract class ProstoAdapter<TBinding : ViewDataBinding, TData> : RecyclerView.Adapter<ProstoHolder<TBinding>>() {      abstract val viewModel: ProstoViewModel<TData>     abstract val layoutId: Int      private var dataSize: Int = 0      open fun setData(data: List<TData>) {         this.dataSize = data.size         viewModel.data = data         notifyDataSetChanged()     }      open var onBind: ((ProstoHolder<TBinding>) -> Unit)? = null      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProstoHolder<TBinding> =         ProstoHolder.create(parent, layoutId)      override fun getItemCount(): Int = dataSize      override fun onBindViewHolder(holder: ProstoHolder<TBinding>, position: Int) {         holder.bind(viewModel, position)         onBind?.invoke(holder)     } }

Новая жизнь

Для создания экземпляра нашего Adapter-a необходимо указать ViewModel c данными, item’s layout id с его типом Binding-класса, который автоматически генерируется на основе layout-а, ну и тип данных для отображения одного item-а.
Также для создания адаптера нет необходимости создавать отдельный класс, но это по желанию 🙂

class MainFragment : Fragment() {     private val adapter =         object : ProstoAdapter<ItemPersonBinding, Person>() {             override val viewModel = PersonItemViewModel()             override val layoutId = R.layout.item_person         }      override fun onActivityCreated(savedInstanceState: Bundle?) {         super.onActivityCreated(savedInstanceState)         mainRecyclerView.adapter = adapter     }      fun setNewPersonList(persons: List<Person>){         adapter.setData(personList)     } }

Итого 4 строки.

Проект https://github.com/klukwist/Prosto

В планах расширить до возможности работы с несколькими ViewHolder-ами.
Всем больше автоматизации 🙂

ссылка на оригинал статьи https://habr.com/ru/post/495762/