Todos escribimos aplicaciones y todos tenemos listas. Y la solución más obvia es RecyclerView. La implementación en sí no es complicada y escribir una guía sobre RecyclerView ya no es relevante. Pero hay una cosa. Cada vez que necesitamos una lista, creamos una clase, prescribimos métodos de plantilla, creamos clases de plantilla. Cuando tenemos 2-3 listas, entonces no hay nada de malo en eso. Pero cuando hay 10 o más de ellos, entonces ya no quieres hacer esto.
Y ante un problema, comencé a buscar. Encontré una implementación muy interesante en Kotlin. Me gustó, pero carecía de algunos elementos. Después de pasar un par de horas, pude finalizarlo y ahora la implementación del adaptador requiere varias líneas. Y aquí quiero compartirlo contigo.
Lo primero que debemos hacer es crear un adaptador.
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) } }
¿Qué está pasando aquí? Creamos un adaptador parametrizado y redefinimos los métodos de plantilla básicos en él. Creamos la interfaz una interfaz Binder parametrizada, que nuestro ViewHolder tendrá que implementar. En el método abstracto getLayoutId (), estableceremos nuestro diseño.
Después de crear una Fábrica para nuestro 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 } } }
Y así es como se verá la implementación de este adaptador en el fragmento.
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 }
Todo es genial, conveniente, rápido. Aproximadamente en esta forma, encontré esta implementación. Pero luego pensé, ¿qué pasa con los elementos cliqueables? Y aquí está mi decisión.
Primero, crea una interfaz
interface OnItemClickListener<T> { fun onClickItem(data: T) }
Y pásalo a nuestra interfaz de carpeta
internal interface Binder<T> { fun bind(data: T, listener: OnItemClickListener<T>?) }
Y en el adaptador, cree un constructor adicional:
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) } } }
Lo que al final tenemos, un adaptador que se crea en 3 líneas y una interfaz universal para todo tipo de elementos. Si no necesitamos procesar clics, simplemente no pasamos el oyente al constructor. Pero eso no es todo.
¿Qué pasa si queremos vincular DiffUtils.Callback a nuestro adaptador?
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 }
Así es como se ve la clase base para nuestros DiffUtils. Agregue un método a nuestro adaptador
private var diffUtil: GenericItemDiff<T>? = null fun setDiffUtilCallback(diffUtilImpl: GenericItemDiff<T>) { diffUtil = diffUtilImpl }
Y modifique ligeramente la actualización del método del adaptador ()
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() } }
Y así implementamos nuestros 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 }
Como resultado, tenemos una implementación simple y bastante flexible del código de la plantilla. Implementación conveniente de adaptadores con múltiples ViewHolders. Lógica centralizada en un solo lugar.
Aquí puedes ver el código fuente .
Y aquí puedes ver la versión original .