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

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

Для примера создан простой data class Person(): с именем, фамилией, эл. почтой и наличием собаки.
Чтобы вывести на экран список людей, необходимо создать RecyclerView.Adapter и RecyclerView.ViewHolder, большая часть кода которых +- одинаковая.
Если у вас один Adapter использует множество разных ViewHolder-ов, эта история не для вас. В большинстве же, наверное, случаев используется один ViewHolder, который просто отображает однотипные данные.
Для таких случаев я сделал базовый Adapter и ViewHolder, чтобы избавиться от рутины.
Обычная жизнь с RecyclerView.Adapter<RecyclerView.ViewHolder>
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) } }
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) } } }
<?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>
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/
Добавить комментарий