Для отображения списка данных мы используем 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 строки.
Проект github.com/klukwist/Prosto
В планах расширить до возможности работы с несколькими ViewHolder-ами.
Всем больше автоматизации :)