Hola a todos! Mis publicaciones son un deseo de ayudar con algunos elementos de Android. Si usted es un desarrollador que aún no ha desarrollado un algoritmo para crear listas, puede resultarle útil leer este material. Básicamente, me gustaría ofrecer soluciones preparadas para el desarrollo, revelando en el curso de la historia algunos pensamientos sobre cómo
llegué a
ellos .
En este articulo:- Formamos varias clases base e interfaces para trabajar con RecyclerView y RecyclerView.
- conecta una biblioteca desde Android Jetpack (opcional, primero sin ella)
- para un desarrollo aún más rápido: la opción de plantilla al final del artículo;)
Entrada
Pues bien! Todos ya se han olvidado de ListView y están escribiendo de forma segura en RecyclerView (
Rv ). Esos momentos en que implementamos el patrón
ViewHolder nos hemos hundido en el olvido.
Rv nos proporciona un conjunto de clases listas para implementar listas y una selección bastante grande de
LayoutManagers para mostrarlas. De hecho, al mirar muchas pantallas, puede imaginar la mayoría de ellas como una lista, precisamente por la capacidad de cada elemento para implementar su propio
ViewHolder .
Nos han contado la historia de desarrollo
con más detalle
en Google I / O.Pero, siempre hay un par de "peros". Las respuestas estándar a Stackoverflow sugieren soluciones generales que conducen a copiar y pegar, especialmente en el lugar donde se implementa el adaptador.
Por el momento,
Rv ya tiene tres años. Hay mucha información al respecto, y hay muchas bibliotecas con soluciones listas para usar, pero ¿qué pasa si no necesita toda la funcionalidad o si sube para mirar el código de otra persona y ve que
Ancient Horror no
es lo que le gustaría ver, o no qué? imaginado? Durante estos tres años, Android finalmente aceptó oficialmente Kotlin = la legibilidad del código ha mejorado, se han publicado muchos artículos interesantes en
Rv que revelan plenamente sus capacidades.
El propósito de esto es recopilar la base de las mejores prácticas de
su bicicleta , un marco para trabajar con listas para nuevas aplicaciones. Este marco se puede complementar con la lógica de una aplicación a otra, utilizando lo que necesita y descartando innecesarios. Creo que este enfoque es mucho mejor que la biblioteca de otra persona: en sus clases tiene la oportunidad de comprender cómo funciona todo y controlar los casos que necesita sin estar atado a la decisión de otra persona.
Pensemos lógicamente y desde el principio
Lo que el componente debe hacer es decidir la
interfaz, no la clase , pero al final cerraremos la lógica de implementación concreta a la clase que implementará e implementará esta interfaz. Pero, si resulta que con la implementación de la interfaz se forma un copiar y pegar, podemos ocultarlo detrás de una clase abstracta y, después, una clase que hereda del resumen. Mostraré mi implementación de las interfaces básicas, pero mi objetivo es que el desarrollador solo trate de pensar en la misma dirección. Una vez más, el plan es este:
un conjunto de interfaces -> una clase abstracta que toma copiar y pegar (si es necesario) -> y ya una clase específica con un código único . Puede implementar interfaces de manera diferente.
¿Qué puede hacer un adaptador con una lista? La respuesta a esta pregunta es más fácil de obtener cuando nos fijamos en algún ejemplo. Puede echar un vistazo al RecyclerView.Adapter, encontrará algunos consejos. Si piensa un poco, puede imaginar algo como estos métodos:
IBaseListAdapterinterface IBaseListAdapter<T> { fun add(newItem: T) fun add(newItems: ArrayList<T>?) fun addAtPosition(pos : Int, newItem : T) fun remove(position: Int) fun clearAll() }
* Al revisar los proyectos, encontré varios otros métodos que omitiré aquí, por ejemplo getItemByPos (position: Int), o incluso subList (startIndex: Int, endIndex: Int). Repito: usted mismo debe mirar lo que necesita del proyecto e incluir funciones en la interfaz. No es difícil cuando sabes que todo sucede en una clase. El ascetismo en este asunto le permitirá deshacerse de la lógica innecesaria, lo que degrada la legibilidad del código, porque una implementación particular requiere más líneas. Presta atención al genérico
T. En general, el adaptador funciona con cualquier objeto de la lista (elemento), por lo que no hay ninguna aclaración aquí; todavía no hemos elegido nuestro enfoque. Y en este artículo habrá al menos dos de ellos, la primera interfaz se ve así:
interface IBaseListItem { fun getLayoutId(): Int }
Bueno, sí, parece lógico: estamos hablando de un elemento de la lista, por lo que cada elemento debe tener algún tipo de diseño, y puede consultarlo usando layoutId. Lo más probable es que un desarrollador novato probablemente no necesite nada, a menos, por supuesto, que
adoptemos enfoques más
avanzados . Si tiene suficiente experiencia en desarrollo, sin duda puede hacer un delegado o un contenedor, pero ¿vale la pena con un proyecto pequeño, y aún menos experiencia en desarrollo? Todos mis enlaces en alguna parte de YouTube son muy útiles si no tiene tiempo en este momento, solo recuérdelos y siga leyendo, porque el enfoque es más simple aquí. Creo que con el trabajo estándar con
Rv , a
juzgar por la documentación oficial , lo que se ofrece arriba no es implícito
Es hora de combinar nuestro
IBaseListAdapter con interfaces, y la siguiente clase será abstracta:
SimpleListAdapter abstract class SimpleListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>(), IBaseListAdapter<IBaseListItem> { protected val items: ArrayList<IBaseListItem> = ArrayList() override fun getItemCount() = items.size override fun getItemViewType(position: Int) = items[position].layoutId protected fun inflateByViewType(context: Context?, viewType: Int, parent: ViewGroup) = LayoutInflater.from(context).inflate(viewType, parent, false) override fun add(newItem: IBaseListItem) { items.add(newItem) notifyDataSetChanged() } override fun add(newItems: ArrayList<IBaseListItem>?) { for (newItem in newItems ?: return) { items.add(newItem) notifyDataSetChanged() } } override fun addAtPosition(pos: Int, newItem: IBaseListItem) { items.add(pos, newItem) notifyDataSetChanged() } override fun clearAll() { items.clear() notifyDataSetChanged() } override fun remove(position: Int) { items.removeAt(position) notifyDataSetChanged() } }
* Nota: Preste atención a la función
getItemViewType (position: Int) anulada. Necesitamos algún tipo de clave int, mediante la cual Rv entenderá qué ViewHolder nos conviene.
Val layoutId para nuestro
artículo es muy útil para esto, ya que Cada vez que Android hace que la identificación de los diseños sea única y todos los valores son mayores que cero, lo utilizaremos más adelante para "
inflar "
itemView para nuestros espectadores en el método
inflateByViewType () (siguiente línea).
Crear una lista
Tomemos, por ejemplo, la pantalla de configuración. Android nos ofrece
su propia versión, pero ¿qué pasa si el diseño necesita algo más sofisticado? Prefiero llenar esta pantalla como una lista. Aquí se dará tal caso:

Vemos dos elementos de lista diferentes, por lo que
SimpleListAdapter y
Rv son perfectos aquí.
¡Empecemos! Puede comenzar con diseños de diseño para item'ov:
item_info.xml; item_switch.xml <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="56dp"> <TextView android:id="@+id/tv_info_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="28dp" android:textColor="@color/black" android:textSize="20sp" tools:text="Balance" /> <TextView android:id="@+id/tv_info_value" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginEnd="48dp" tools:text="1000 $" /> </FrameLayout> <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="56dp"> <TextView android:id="@+id/tv_switch_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="28dp" android:textColor="@color/black" android:textSize="20sp" tools:text="Send notifications" /> <Switch android:id="@+id/tv_switch_value" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginEnd="48dp" tools:checked="true" /> </FrameLayout>
Luego, definimos las clases mismas, dentro de las cuales queremos pasar los valores que interactúan con la lista: el primero es el encabezado y algún valor que vino del exterior (tendremos un código auxiliar, sobre solicitudes en otro momento), el segundo es el encabezado y la variable booleana por el cual debemos realizar una acción. Para distinguir entre los elementos Switch, la identificación de las entidades del servidor es adecuada, si no están allí, podemos crearlos nosotros mismos durante la inicialización.
InfoItem.kt, SwitchItem.kt class InfoItem(val title: String, val value: String): IBaseListItem { override val layoutId = R.layout.item_info } class SwitchItem( val id: Int, val title: String, val actionOnReceive: (itemId: Int, userChoice: Boolean) -> Unit ) : IBaseListItem { override val layoutId = R.layout.item_switch }
En una implementación simple, cada elemento también necesitará un ViewHolder:
InfoViewHolder.kt, SwitchViewHolder.kt class InfoViewHolder.kt(view: View) : RecyclerView.ViewHolder(view) { val tvTitle = view.tv_info_title val tvValue = view.tv_info_value } class SwitchViewHolder.kt(view: View) : RecyclerView.ViewHolder(view) { val tvTitle = view.tv_switch_title val tvValue = view.tv_switch_value }
Bueno, la parte más interesante es la implementación concreta de SimpleListAdapter'a:
SettingsListAdapter.kt class SettingsListAdapter : SimpleListAdapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val context = parent.context return when (viewType) { R.layout.item_info -> InfoHolder(inflateByViewType(context, viewType, parent)) R.layout.item_switch -> SwitchHolder(inflateByViewType(context, viewType, parent)) else -> throw IllegalStateException("There is no match with current layoutId") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is InfoHolder -> { val infoItem = items[position] as InfoItem holder.tvTitle.text = infoItem.title holder.tvValue.text = infoItem.value } is SwitchHolder -> { val switchItem = items[position] as SwitchItem holder.tvTitle.text = switchItem.title holder.tvValue.setOnCheckedChangeListener { _, isChecked -> switchItem.actionOnReceive.invoke(switchItem.id, isChecked) } } else -> throw IllegalStateException("There is no match with current holder instance") } } }
* Nota: No olvide que bajo el capó del método
inflateByViewType (contexto, viewType, padre): viewType = layoutId.¡Todos los componentes están listos! Ahora, el código de actividad permanece y puede ejecutar el programa:
activity_settings.xml <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/rView" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
SettingsActivity.kt class SettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) val adapter = SettingsListAdapter() rView.layoutManager = LinearLayoutManager(this) rView.adapter = adapter adapter.add(InfoItem("User Name", "Leo Allford")) adapter.add(InfoItem("Balance", "350 $")) adapter.add(InfoItem("Tariff", "Business")) adapter.add(SwitchItem(1, "Send Notifications") { itemId, userChoice -> onCheck(itemId, userChoice) }) adapter.add(SwitchItem(2, "Send News on Email") { itemId, userChoice -> onCheck(itemId, userChoice) }) } private fun onCheck(itemId: Int, userChoice: Boolean) { when (itemId) { 1 -> Toast.makeText(this, "Notification now set as $userChoice", Toast.LENGTH_SHORT).show() 2 -> Toast.makeText(this, "Send news now set as $userChoice", Toast.LENGTH_SHORT).show() } } }
Como resultado, al crear la lista, todo el trabajo se reduce a lo siguiente:
1. Cálculo del
número de diseños diferentes para artículos
2. Elige sus
nombres . Uso la regla:
Something Item.kt, item_
something .xml,
Something ViewHolder.kt
3. Escribimos un
adaptador para estas clases. En principio, si no pretende optimizar, entonces un adaptador común es suficiente. Pero en proyectos grandes, todavía haría algunos, en las pantallas, porque en el primer caso el método
onBindViewHolder () inevitablemente crece (la legibilidad del código sufre) en su adaptador (en nuestro caso, es
SettingsListAdapter ) + el programa tendrá que ir cada vez, para cada elemento, ejecutar en este método + método
onCreateViewHolder ()4. Ejecute el código y disfrute!
Jetpack
Hasta ese momento, utilizamos el enfoque estándar de enlace de datos de
Item.kt a nuestro
item_layout.xml . Pero podemos unificar el método
onBindViewHolder () , dejarlo al mínimo y transferir la lógica a Item y layout.
Vayamos a la página oficial de Android JetPack:

Prestemos atención a la primera pestaña en la sección Arquitectura.
Android Databinding es un tema muy extenso, me gustaría hablar de él con más detalle en otros artículos, pero por ahora lo usaremos solo como parte del actual, haremos que nuestro
Item.kt sea variable para
item.xml (o puede llamarlo un modelo de vista para el diseño).
Al momento de escribir,
Databinding podría conectarse de esta manera:
android { compileSdkVersion 27 defaultConfig {...} buildTypes {...} dataBinding { enabled = true } dependencies { kapt "com.android.databinding:compiler:3.1.3"
Repasemos las clases base nuevamente. La interfaz para el artículo complementa la anterior:
interface IBaseItemVm: IBaseListItem { val brVariableId: Int }
Además, ampliaremos nuestro ViewHolder, por lo tanto, nos contactaremos con databyding. Le pasaremos
ViewDataBinding , después de lo cual nos olvidaremos de forma segura de crear un diseño y un enlace de datos
class VmViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
Aquí se usa el mismo enfoque, pero en Kotlin parece mucho más corto, ¿no? =)
VmListAdapter class VmListAdapter : RecyclerView.Adapter<VmViewHolder>(), IBaseListAdapter<IBaseItemVm> { private var mItems = ArrayList<IBaseItemVm>() override fun getItemCount() = mItems.size override fun getItemViewType(position: Int) = mItems[position].layoutId override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VmViewHolder { val inflater = LayoutInflater.from(parent.context) val viewDataBinding = DataBindingUtil.inflate<ViewDataBinding>(inflater!!, viewType, parent, false) return VmViewHolder(viewDataBinding) } override fun onBindViewHolder(holder: VmViewHolder, position: Int) { holder.binding.setVariable(mItems[position].brVariableId, mItems[position]) holder.binding.executePendingBindings() } override fun add(newItem: IBaseItemVm) { mItems.add(newItem) notifyItemInserted(mItems.lastIndex) } override fun add(newItems: ArrayList<IBaseItemVm>?) { val oldSize = mItems.size mItems.addAll(newItems!!) notifyItemRangeInserted(oldSize, newItems.size) } override fun clearAll() { mItems.clear() notifyDataSetChanged() } override fun getItemId(position: Int): Long { val pos = mItems.size - position return super.getItemId(pos) } override fun addAtPosition(pos: Int, newItem: IBaseItemVm) { mItems.add(pos, newItem) notifyItemInserted(pos) } override fun remove(position: Int) { mItems.removeAt(position) notifyItemRemoved(position) } }
En general, preste atención a los métodos
onCreateViewHolder () ,
onBindViewHolder () . La idea es que ya no crecen. Total, obtienes un adaptador para cualquier pantalla, con cualquier elemento de la lista.
Nuestros artículos:
InfoItem.kt, SwitchItem.kt class InfoItem(val title: String, val value: String) : IBaseItemVm { override val brVariableId = BR.vmInfo override val layoutId = R.layout.item_info } // class SwitchItem( val id: Int, val title: String, private val actionOnReceive: (itemId: Int, userChoice: Boolean) -> Unit ) : IBaseItemVm { override val brVariableId = BR.vmSwitch override val layoutId = R.layout.item_switch val listener = CompoundButton.OnCheckedChangeListener { _, isChecked -> actionOnReceive.invoke(id, isChecked) } }
Aquí puede ver dónde fue la lógica del método
onBindViewHolder () . Android Databinding lo asumió: ahora cualquiera de nuestro diseño está respaldado por su modelo de vista, y procesará con calma toda la lógica de clics, animaciones, consultas y otras cosas. ¿Qué se te ocurre?
Los adaptadores de enlace harán un buen trabajo de esto: le permiten conectar la vista con datos de cualquier tipo. Además, la comunicación se puede mejorar gracias al
enlace de datos bidireccional . Probablemente parpadeará en cualquiera de los siguientes artículos, en este ejemplo todo se puede simplificar. Un adaptador adaptador es suficiente para nosotros:
@BindingAdapter("switchListener") fun setSwitchListener(sw: Switch, listener: CompoundButton.OnCheckedChangeListener) { sw.setOnCheckedChangeListener(listener) }
Después de eso, vinculamos nuestros valores variables con nuestro
artículo dentro de xml:
item_info.xml; item_switch.xml <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="com.lfkekpoint.adapters.adapters.presentation.modules.bindableItemsSettings.InfoItem" /> <variable name="vmInfo" type="InfoItem" /> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="56dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="28dp" android:text="@{vmInfo.title}" android:textColor="@color/black" android:textSize="20sp" tools:text="Balance" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginEnd="48dp" android:text="@{vmInfo.value}" tools:text="1000 $" /> </FrameLayout> </layout> <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> <import type="com.lfkekpoint.adapters.adapters.presentation.modules.bindableItemsSettings.SwitchItem" /> <variable name="vmSwitch" type="SwitchItem" /> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="56dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginStart="28dp" android:text="@{vmSwitch.title}" android:textColor="@color/black" android:textSize="20sp" tools:text="Send notifications" /> <Switch android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|end" android:layout_marginEnd="48dp" app:switchListener="@{vmSwitch.listener}" tools:checked="true" /> </FrameLayout> </layout>
aplicación: switchListener = "@ {vmSwitch.listener}" - en esta línea usamos nuestro
BindingAdapter * Nota: Por razones justas, a algunos les puede parecer que escribimos mucho más código en xml, pero se trata de conocer la biblioteca de enlace de datos de Android. Complementa el diseño, se lee rápidamente y, en principio, en su mayor parte elimina con precisión la caldera. Creo que Google va a desarrollar bien esta biblioteca, ya que es la primera en la pestaña Arquitectura en Android Jetpack. Intente cambiar MVP a MVVM en un par de proyectos, y muchos se sorprenderán gratamente.
Bueno, entonces! .. Ah, el código en SettingsActivity:
SettingsActivity.kt... no ha cambiado, a menos que el adaptador haya cambiado. =) Pero para no saltar sobre el artículo:
class SettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_settings) val adapter = BaseVmListAdapter() rView.layoutManager = LinearLayoutManager(this) rView.adapter = adapter adapter.add(InfoItem("User Name", "Leo Allford")) adapter.add(InfoItem("Balance", "350 $")) adapter.add(InfoItem("Tariff", "Business")) adapter.add(SwitchItem(1, "Send Notifications") { itemId, userChoice -> onCheck(itemId, userChoice) }) adapter.add(SwitchItem(2, "Send News on Email") { itemId, userChoice -> onCheck(itemId, userChoice) }) } private fun onCheck(itemId: Int, userChoice: Boolean) { when (itemId) { 1 -> Toast.makeText(this, "Notification now set as $userChoice", Toast.LENGTH_SHORT).show() 2 -> Toast.makeText(this, "Send news now set as $userChoice", Toast.LENGTH_SHORT).show() } } }
Resumen
Tenemos un algoritmo para crear listas y herramientas para trabajar con ellos. En mi caso (casi siempre uso
Databinding ), toda la preparación se reduce a la inicialización de las clases base en carpetas, el diseño de los elementos en .xml y luego el enlace a las variables en .kt.
Acelerando el desarrolloPara un trabajo más rápido, utilicé las
plantillas de Apache para Android Studio, y escribí
mis plantillas con una pequeña
demostración de cómo funciona todo. Realmente espero que alguien sea útil. Tenga en cuenta que cuando trabaja, debe llamar a la plantilla desde la carpeta raíz del proyecto; esto se hace porque el parámetro
applicationId del proyecto puede mentirle si lo cambia en Gradle. Pero
packageName no puede ser
engañado tan fácilmente, lo cual usé. El lenguaje disponible sobre la plantilla se puede leer en los enlaces a continuación
Referencias / Medios
1.
Desarrollo moderno de Android: Android Jetpack, Kotlin y más (Google I / O 2018, 40 m.) : Una breve guía de lo que está de moda hoy, a partir de aquí también quedará claro en términos generales cómo se desarrolló RecyclerView;
2.
Droidcon NYC 2016 - Radical RecyclerView, 36 m. - Un informe detallado sobre RecyclerView de
Lisa Wray ;
3.
Cree una lista con RecyclerView - documentación oficial
4.
Interfaces vs. clases5.
Formato de plantilla IDE de Android ,
plantillas totales ,
manual de FreeMarker : un enfoque conveniente, que en el marco de este artículo ayudará a crear rápidamente los archivos necesarios para trabajar con listas
6.
Código para el artículo (hay nombres de clase ligeramente diferentes, tenga cuidado),
plantillas para trabajo y
video, cómo trabajar con plantillas7. Versión en
inglés del artículo.