Все мы пишем приложения и у всех нас есть списки. И самое очевидное решение это RecyclerView. Сама по себе реализация не сложна и писать гайд по RecyclerView уже не актуально. Но есть одно но. Каждый раз когда нам нужен список мы создаем класс, в нем прописываем шаблонный методы, создаем шаблонные классы. Когда у нас 2-3 списка то ничего страшного в этом нет. Но когда их 10 или того более, то этого делать уже не хочется.

И вот столкнувшись с проблемой я начал искать. Нашел одну очень интересную реализацию на 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)


  1. agent10
    30.09.2019 19:55
    +1

    Вброшу немного. Пробовал разные решения-обёртки над RecyclerView. Но это не таблетка счастья для меня. И вот почему:
    1) Я не создаю по 5 экранов-списков на дню, чтобы меня это действительно мучало.
    2) Я прекрасно знаю как заставить работать стандартный RecyclerView, а вот та обёртка которую использовал последний раз 2 месяца назад уже не помню… хочу ли вспоминать — не особо.
    3) Бывает нужно сделать что-то хитрое, на что обёртка не способна… и вот ты либо выкидываешь полуготовый код и переходишь на обычный RecyclerView, или дорабатываешь обёртку(а тут уже п.2 снова)


    1. wisp_cool_wisp Автор
      30.09.2019 23:18

      Ну вот у меня в проекте как минимум 10ок таких простых ресайклеров. Тут многое зависит от проекта. Да есть конечно и сложные списки и там придется реализовывать руками все. А есть те кто пишут кучу однотипных адаптеров изо дня в день


      1. agent10
        30.09.2019 23:21

        Верно, я бы сказал, что зависит не от проектов, а от их сменяемости. Если проектов сотни и каждый по месяцу, то наверное такая обёртка имеет смысл. А если один на пару лет…


  1. linyaKoder
    30.09.2019 20:02
    +1

    Честно говоря, вы не объяснили, что вам не хватило в предыдущей реализации и что именно вы решили дописывать. Поэтому статья сразу становится непонятной.


    1. wisp_cool_wisp Автор
      30.09.2019 23:20

      Потому что в предыдущей реализации не было ни диф утилсов ни клик листенера. Так же для ресайклера с 1им View Holder я вынес реализацию получения ViewHolder в абстрактный метод


  1. zagayevskiy
    30.09.2019 21:27

    Давно уже есть более или менее стандартное решение — Adapter Delegates.
    Diff лучше считать на другом треде.


  1. ZiggiPop
    30.09.2019 22:23
    -1

    Все мы пишем приложения

    Нет.


    О чем вообще этот труд?