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

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

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

Для отображения списка данных мы используем 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-ами.


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

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

Публикации

Истории

Работа

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

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