[Из песочницы] Generic Recycler View или как не писать шаблонный код

?v=1

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

И вот столкнувшись с проблемой я начал искать. Нашел одну очень интересную реализацию на Kotlin. Она мне понравилась, но в ней не хватало нескольких элементов. Потратив еще пару часов, я смог доработать его и теперь реализация адаптера занимает несколько строчек. И здесь я хочу поделиться ею с вами.

Первое что нам необходимо сделать это создать адаптер.

abstract class GenericAdapter : RecyclerView.Adapter {
    private var itemList = mutableListOf()

    constructor()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return ViewHolderFactory.create(
            LayoutInflater.from(parent.context)
                .inflate(viewType, parent, false)
            , viewType
        )
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        (holder as Binder).bind(itemList[position], itemClickListener)
    }

    override fun getItemCount(): Int = itemList.size

    override fun getItemViewType(position: Int): Int = getLayoutId(position, itemList[position])

    fun update(items: List) {
            itemList = items.toMutableList()
            notifyDataSetChanged()
    }

    protected abstract fun getLayoutId(position: Int, obj: T): Int

    internal interface Binder {
        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 {
        override fun bind(data: Data) {
            itemView.apply {
                dateTextView.text = data.dateTitle
            }
        }

class OtherDataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
        GenericAdapter.Binder {
        override fun bind(data: OtherData) {
            itemView.apply {
                dateTextView.text = data.dateTitle
            }
        }
}


И вот так будет выглядеть реализация этого адаптера во фрагменте.


private lateinit var adapter GenericAdapter

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        adapter = dataAdapter
}

private val dataAdapter = object : GenericAdapter() {
        override fun getLayoutId(position: Int, obj: Data): Int =
            R.layout.item_data
}


Все классно, удобно, быстро. Примерно в таком виде я нашел эту реализацию. Но тут я подумал, а как же быть с кликабельными элементами. И вот мое решение.

Для начала создадим интерфейс

interface OnItemClickListener {
    fun onClickItem(data: T)
}


И передадим его в наш интерфейс Binder

internal interface Binder {
        fun bind(data: T, listener: OnItemClickListener?)
}


А в адаптере создадим дополнительный конструктор:

 private var itemClickListener: OnItemClickListener? = null

constructor(listener: OnItemClickListener) {
        itemClickListener = listener
}

class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
        GenericAdapter.Binder {
        override fun bind(data: Data, listener: OnItemClickListener?) {
            itemView.apply {
                dateTextView.text = data.dateTitle
                setOnClickListener { listener?.onClickItem(data) }
            }
        }


Что в итоге мы имеем, адаптер который создается в 3 строчки и универсальный интерфейс для всех видом элементов. Если же у нас нет необходимости обрабатывать клики, то мы просто напросто не передаем слушатель в конструктор. Но и это еще не все.

А вдруг мы захотим привязать к нашему адаптеру DiffUtils.Callback.

class GenericDiffUtil(
    private val oldItems: List,
    private val newItems: List,
    private val itemDiff: GenericItemDiff
) :
    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 {
    fun isSame(
        oldItems: List,
        newItems: List,
        oldItemPosition: Int,
        newItemPosition: Int
    ): Boolean

    fun isSameContent(
        oldItems: List,
        newItems: List,
        oldItemPosition: Int,
        newItemPosition: Int
    ): Boolean
}


Вот так выглядит базовый класс для наших DiffUtils. Добавляем в наш адаптер метод

private var diffUtil: GenericItemDiff? = null

fun setDiffUtilCallback(diffUtilImpl: GenericItemDiff) {
        diffUtil = diffUtilImpl
    }


И немного модифицируем метод адаптера update ()

 fun update(items: List) {
        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 {
        override fun isSame(
            oldItems: List,
            newItems: List,
            oldItemPosition: Int,
            newItemPosition: Int
        ): Boolean {
            val oldData = oldItems[oldItemPosition]
            val newData = newItems[newItemPosition]
            return oldData.id == newData.id
        }

        override fun isSameContent(
            oldItems: List,
            newItems: List,
            oldItemPosition: Int,
            newItemPosition: Int
        ): Boolean {
            val oldData = oldItems[oldItemPosition]
            val newData = newItems[newItemPosition]
            return oldData.name == newData.name && oldData.content == newData.content
        }


В итоге мы имеем простую и достаточно гибкую реализацию шаблонного кода. Удобную реализацию адаптеров с несколькими ViewHolders. Централизованную логику в одном месте.

Здесь есть можно посмотреть исходный код.

А здесь можно посмотреть исходную версию.

© Habrahabr.ru