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 строки.


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


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


    Всем больше автоматизации :)

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 5

      0
      Есть еще такая крутейшая обертка вокруг адаптеров, как FastAdapter
        0
        Советую Groupie для всех действий связанных с RecyclerView.
          0
          Зачем ProstoViewModel наследуется от ViewModel?
            0
            Нужно для моих задач. Конкретно в данном простом примере необходимости нет.
            0
            Намного проще через kotlin dsl, библиотека

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое