Все мы пишем приложения и у всех нас есть списки. И самое очевидное решение это RecyclerView. Сама по себе реализация не сложна и писать гайд по RecyclerView уже не актуально. Но есть одно но. Каждый раз когда нам нужен список мы создаем класс, в нем прописываем шаблонный методы, создаем шаблонные классы. Когда у нас 2-3 списка то ничего страшного в этом нет. Но когда их 10 или того более, то этого делать уже не хочется.
И вот столкнувшись с проблемой я начал искать. Нашел одну очень интересную реализацию на Kotlin. Она мне понравилась, но в ней не хватало нескольких элементов. Потратив еще пару часов, я смог доработать его и теперь реализация адаптера занимает несколько строчек. И здесь я хочу поделиться ею с вами.
Первое что нам необходимо сделать это создать адаптер.
Что у нас здесь происходит? Мы создаем параметрезированный адаптер и переопределяем в нем базовые шаблонный методы. Создаем интерфейс параметризированный интерфейс Binder, который должны будут реализовать наши ViewHolder. В абстрактном методе getLayoutId() мы будет задавать наш макет.
После мы создаем Фабрику для наших ViewHolder.
И вот так будет выглядеть реализация этого адаптера во фрагменте.
Все классно, удобно, быстро. Примерно в таком виде я нашел эту реализацию. Но тут я подумал, а как же быть с кликабельными элементами. И вот мое решение.
Для начала создадим интерфейс
И передадим его в наш интерфейс Binder
А в адаптере создадим дополнительный конструктор:
Что в итоге мы имеем, адаптер который создается в 3 строчки и универсальный интерфейс для всех видом элементов. Если же у нас нет необходимости обрабатывать клики, то мы просто напросто не передаем слушатель в конструктор. Но и это еще не все.
А вдруг мы захотим привязать к нашему адаптеру DiffUtils.Callback.
Вот так выглядит базовый класс для наших DiffUtils. Добавляем в наш адаптер метод
И немного модифицируем метод адаптера update()
И вот так мы реализуем наш DiffUtils
В итоге мы имеем простую и достаточно гибкую реализацию шаблонного кода. Удобную реализацию адаптеров с несколькими ViewHolders. Централизованную логику в одном месте.
Здесь есть можно посмотреть исходный код.
А здесь можно посмотреть исходную версию.
И вот столкнувшись с проблемой я начал искать. Нашел одну очень интересную реализацию на Kotlin. Она мне понравилась, но в ней не хватало нескольких элементов. Потратив еще пару часов, я смог доработать его и теперь реализация адаптера занимает несколько строчек. И здесь я хочу поделиться ею с вами.
Первое что нам необходимо сделать это создать адаптер.
abstract class GenericAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder> {
private var itemList = mutableListOf<T>()
constructor()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return getViewHolder(
LayoutInflater.from(parent.context)
.inflate(viewType, parent, false)
, viewType
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as Binder<T>).bind(itemList[position], itemClickListener)
}
override fun getItemCount(): Int = itemList.size
override fun getItemViewType(position: Int): Int = getLayoutId(position, itemList[position])
fun update(items: List<T>) {
itemList = items.toMutableList()
notifyDataSetChanged()
}
protected abstract fun getLayoutId(position: Int, obj: T): Int
protected open fun getViewHolder(view: View, viewType: Int): RecyclerView.ViewHolder {
return ViewHolderFactory.create(view, viewType)
}
internal interface Binder<T> {
fun bind(data: T)
}
}
Что у нас здесь происходит? Мы создаем параметрезированный адаптер и переопределяем в нем базовые шаблонный методы. Создаем интерфейс параметризированный интерфейс Binder, который должны будут реализовать наши ViewHolder. В абстрактном методе getLayoutId() мы будет задавать наш макет.
После мы создаем Фабрику для наших ViewHolder.
object ViewHolderFactory {
fun create(view: View, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
R.layout.item_data -> DataViewHolder(view)
R.layout.item_other_data -> OtherDataViewHolder(view)
else -> throw Exception("Wrong view type")
}
}
class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
GenericAdapter.Binder<Data> {
override fun bind(data: Data) {
itemView.apply {
dateTextView.text = data.dateTitle
}
}
class OtherDataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
GenericAdapter.Binder<OtherData> {
override fun bind(data: OtherData) {
itemView.apply {
dateTextView.text = data.dateTitle
}
}
}
И вот так будет выглядеть реализация этого адаптера во фрагменте.
private lateinit var adapter GenericAdapter<Data>
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
adapter = dataAdapter
}
private val dataAdapter = object : GenericAdapter<Data>() {
override fun getLayoutId(position: Int, obj: Data): Int =
R.layout.item_data
}
Все классно, удобно, быстро. Примерно в таком виде я нашел эту реализацию. Но тут я подумал, а как же быть с кликабельными элементами. И вот мое решение.
Для начала создадим интерфейс
interface OnItemClickListener<T> {
fun onClickItem(data: T)
}
И передадим его в наш интерфейс Binder
internal interface Binder<T> {
fun bind(data: T, listener: OnItemClickListener<T>?)
}
А в адаптере создадим дополнительный конструктор:
private var itemClickListener: OnItemClickListener<T>? = null
constructor(listener: OnItemClickListener<T>) {
itemClickListener = listener
}
class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
GenericAdapter.Binder<Data> {
override fun bind(data: Data, listener: OnItemClickListener<Data>?) {
itemView.apply {
dateTextView.text = data.dateTitle
setOnClickListener { listener?.onClickItem(data) }
}
}
Что в итоге мы имеем, адаптер который создается в 3 строчки и универсальный интерфейс для всех видом элементов. Если же у нас нет необходимости обрабатывать клики, то мы просто напросто не передаем слушатель в конструктор. Но и это еще не все.
А вдруг мы захотим привязать к нашему адаптеру DiffUtils.Callback.
class GenericDiffUtil<T>(
private val oldItems: List<T>,
private val newItems: List<T>,
private val itemDiff: GenericItemDiff<T>
) :
DiffUtil.Callback() {
override fun getOldListSize() = oldItems.size
override fun getNewListSize() = newItems.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
itemDiff.isSame(oldItems, newItems, oldItemPosition, newItemPosition)
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
itemDiff.isSameContent(oldItems, newItems, oldItemPosition, newItemPosition)
}
interface GenericItemDiff<T> {
fun isSame(
oldItems: List<T>,
newItems: List<T>,
oldItemPosition: Int,
newItemPosition: Int
): Boolean
fun isSameContent(
oldItems: List<T>,
newItems: List<T>,
oldItemPosition: Int,
newItemPosition: Int
): Boolean
}
Вот так выглядит базовый класс для наших DiffUtils. Добавляем в наш адаптер метод
private var diffUtil: GenericItemDiff<T>? = null
fun setDiffUtilCallback(diffUtilImpl: GenericItemDiff<T>) {
diffUtil = diffUtilImpl
}
И немного модифицируем метод адаптера update()
fun update(items: List<T>) {
if (diffUtil != null) {
val result = DiffUtil.calculateDiff(GenericDiffUtil(itemList, items, diffUtil!!))
itemList.clear()
itemList.addAll(items)
result.dispatchUpdatesTo(this)
} else {
itemList = items.toMutableList()
notifyDataSetChanged()
}
}
И вот так мы реализуем наш DiffUtils
adapter.setDiffUtilCallback(dataDiffUtil)
private val dataDiffUtil = object : GenericItemDiff<Data> {
override fun isSame(
oldItems: List<Data>,
newItems: List<Data>,
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldData = oldItems[oldItemPosition]
val newData = newItems[newItemPosition]
return oldData.id == newData.id
}
override fun isSameContent(
oldItems: List<Data>,
newItems: List<Data>,
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldData = oldItems[oldItemPosition]
val newData = newItems[newItemPosition]
return oldData.name == newData.name && oldData.content == newData.content
}
В итоге мы имеем простую и достаточно гибкую реализацию шаблонного кода. Удобную реализацию адаптеров с несколькими ViewHolders. Централизованную логику в одном месте.
Здесь есть можно посмотреть исходный код.
А здесь можно посмотреть исходную версию.
Комментарии (7)
linyaKoder
30.09.2019 20:02+1Честно говоря, вы не объяснили, что вам не хватило в предыдущей реализации и что именно вы решили дописывать. Поэтому статья сразу становится непонятной.
wisp_cool_wisp Автор
30.09.2019 23:20Потому что в предыдущей реализации не было ни диф утилсов ни клик листенера. Так же для ресайклера с 1им View Holder я вынес реализацию получения ViewHolder в абстрактный метод
zagayevskiy
30.09.2019 21:27Давно уже есть более или менее стандартное решение — Adapter Delegates.
Diff лучше считать на другом треде.
agent10
Вброшу немного. Пробовал разные решения-обёртки над RecyclerView. Но это не таблетка счастья для меня. И вот почему:
1) Я не создаю по 5 экранов-списков на дню, чтобы меня это действительно мучало.
2) Я прекрасно знаю как заставить работать стандартный RecyclerView, а вот та обёртка которую использовал последний раз 2 месяца назад уже не помню… хочу ли вспоминать — не особо.
3) Бывает нужно сделать что-то хитрое, на что обёртка не способна… и вот ты либо выкидываешь полуготовый код и переходишь на обычный RecyclerView, или дорабатываешь обёртку(а тут уже п.2 снова)
wisp_cool_wisp Автор
Ну вот у меня в проекте как минимум 10ок таких простых ресайклеров. Тут многое зависит от проекта. Да есть конечно и сложные списки и там придется реализовывать руками все. А есть те кто пишут кучу однотипных адаптеров изо дня в день
agent10
Верно, я бы сказал, что зависит не от проектов, а от их сменяемости. Если проектов сотни и каждый по месяцу, то наверное такая обёртка имеет смысл. А если один на пару лет…