Hola a todos. Decidí plantear el tema de las listas complejas en Android. Este tema me desconcertó al principio. Me pareció muy difícil, pero en realidad todo es más sencillo. Y creo que cualquiera que se enfrente ahora a este problema encontrará útil el artículo. Todos los ejemplos están escritos en Kotlin. Intenté escribir comentarios en todas partes y hacerlo lo más claro posible para quienes escriben en Java. Aquí está nuestra estructura de clases:
MainActivity
- Esta es la clase principal. ListAdapter
- Aquí vinculamos nuestra lista para ver elementos. ViewHolder
(hay varios): este es nuestro marcado para cada tipo de elemento. Las clases de elementos son datos para una lista (pojo). Los archivos xml son marcas ( activity_main
para la pantalla principal, el resto para cada tipo de elemento de la lista). Lo primero que debemos hacer es agregar una dependencia para que podamos usar listas (por alguna razón, esta dependencia falta al crear un proyecto). En el archivo build.gradle
del bloque dependencies
agregamos la línea:
implementation "androidx.recyclerview:recyclerview:1.1.0"
A continuación encontramos el archivo activity_main.xml
: Aquí sólo está nuestro elemento para listas - RecyclerView
. Agregamos sangrías y formato al gusto. A continuación creamos la clase BaseViewHolder
. Será abstracto, ya que todas nuestras clases heredarán de él ViewHolder
. Es muy posible prescindir de él, pero aún así recomiendo asegurarse de que todas las clases heredadas tengan la misma estructura. (en Kotlin, extends
y implement
se usa en su lugar :
. Como en Java, aquí puedes heredar de una clase e implementar muchas interfaces) A continuación, creamos nuestras clases de datos ItemTitle
: ItemGoogle
y ItemApple
. En Java, estas son clases ordinarias con un constructor y captadores.
data class ItemGoogle(
val name: String,
val product: String,
val version: String,
val isUse: Boolean
) : ListMarker
data class ItemApple(
val name: String,
val country: String,
val year: Int
) : ListMarker
data class ItemTitle(
val title: String,
val amount: Int
) : ListMarker
Centrémonos aquí en la interfaz ListMarker
.
//это наш маркер. Его должны реализовать все айтемы, которые будут
//отображаться в конечном итоге в списке
interface ListMarker
Todas nuestras clases de datos implementan esta interfaz para convertirlas al mismo tipo. Necesitaremos esto más adelante para nuestra lista. A continuación, creamos nuestros visualizadores para cada elemento de la lista. No olvides que cada uno de ellos hereda de BaseViewHolder
. En estas clases especificamos qué elemento view
corresponde al campo de las clases de datos.
abstract class BaseViewHolder<t>(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(item: T)
}
class GoogleViewHolder(view: View) : BaseViewHolder<itemgoogle>(view) {
override fun bind(item: ItemGoogle) {
itemView.tvName.text = item.name
itemView.tvProduct.text = item.product
itemView.tvVersion.text = item.version
if (item.isUse) {
itemView.tvProduct.visibility = View.GONE
} else {
itemView.tvProduct.visibility = View.VISIBLE
}
}
}
class AppleViewHolder(view: View) : BaseViewHolder<itemapple>(view) {
override fun bind(item: ItemApple) {
//можем делать так
itemView.tvName.text = item.name
itemView.tvCountry.text = item.country
itemView.tvYear.text = item.year.toString()
/*----сверху и снизу два идентичных блока----*/
//а можем сделать такой блок и не использовать в каждой строке itemView
with(itemView) {
tvName.text = item.name
tvCountry.text = item.country
tvYear.text = item.year.toString()
}
}
}
class TitleViewHolder(view: View) : BaseViewHolder<itemtitle>(view) {
override fun bind(item: ItemTitle) {
itemView.tvTitle.text = item.title
itemView.tvAmount.text = item.amount.toString()
}
}
No hay ninguna lógica particular aquí, todo es extremadamente simple. Además, cada visor tiene su propio diseño: list_item_title.xml
list_item_google.xml
list_item_apple.xml
ahora pasemos a la parte más difícil: ListAdapter
.
class ListAdapter : RecyclerView.Adapter<baseviewholder<*>>() {
companion object {
//задаем константы для каждого типа айтема
private const val TYPE_TITLE = 0
private const val TYPE_GOOGLE = 1
private const val TYPE_APPLE = 2
}
//здесь можно использовать обычный ArrayList
//сюда добавляются все айтемы, которые реализовали интерфейс ListMarker
//Cómo вариант можно было сделать mutableListOf<any>() и обойтись без интерфейса
private val items = mutableListOf<listmarker>()
internal fun swapData(list: List<listmarker>) {
items.clear()
items.addAll(list)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return when(viewType) {
//задаем разметку для каждого типа айтема
TYPE_TITLE -> TitleViewHolder(parent.inflate(R.layout.list_item_title))
TYPE_GOOGLE -> GoogleViewHolder(parent.inflate(R.layout.list_item_google))
TYPE_APPLE -> AppleViewHolder(parent.inflate(R.layout.list_item_apple))
else -> throw IllegalArgumentException("Invalid view type")
}
}
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is ItemTitle -> TYPE_TITLE
is ItemGoogle -> TYPE_GOOGLE
is ItemApple -> TYPE_APPLE
else -> throw IllegalArgumentException("Invalid type of item $position")
}
}
override fun getItemCount(): Int {
//этот метод определяет размер списка
return items.size
}
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
val element = items[position]
when (holder) {
//отправляем каждый айтем к своему ViewHolder
is TitleViewHolder -> holder.bind(element as ItemTitle)
is GoogleViewHolder -> holder.bind(element as ItemGoogle)
is AppleViewHolder -> holder.bind(element as ItemApple)
else -> throw IllegalArgumentException()
}
}
}
</listmarker></listmarker></any></baseviewholder<*>
Cuando heredamos nuestra clase de RecyclerView.Adapter
, necesitaremos anular tres métodos: onCreateViewHolder
, getItemCount
, onBindViewHolder
. onCreateViewHolder
-Aquí inicializamos nuestras clases ViewHolder
. getItemCount
— este método es responsable del tamaño de la lista. onBindViewHolder
-Aquí pasamos los elementos de la lista a nuestras clases ViewHolder
. Para una lista normal con un tipo de elemento, esto sería suficiente. Pero para diferentes tipos todavía necesitamos redefinir el método getItemViewType
(para esto usamos las constantes que están en la parte superior de nuestra clase de adaptador. En Java puedes usar final
variables para esto). También en Java, onBindViewHolder
en lugar de una expresión, when
puedes usar un archivo if
. Y finalmente, pasemos a nuestra clase principal: MainActivity
.
class MainActivity : AppCompatActivity() {
private val adapter = ListAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initRecyclerView()
}
private fun initRecyclerView() {
rvList.layoutManager = LinearLayoutManager(this)
//здесь мы задаем разделитель между айтемами, чтоб они не сливались друг с другом
val divider = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
rvList.addItemDecoration(divider)
rvList.adapter = adapter
stubData()
}
private fun stubData() {
val list = mutableListOf<listmarker>()
list.add(ItemTitle("title1", 4))
list.add(ItemGoogle("android", "product1", "17.0v", true))
list.add(ItemGoogle("no name", "product2", "3.1v", false))
list.add(ItemApple("macOs", "USA", 2005))
list.add(ItemApple("iOs", "China", 2007))
list.add(ItemTitle("title2", 2))
list.add(ItemGoogle("map", "product3", "23.0v", true))
list.add(ItemApple("car", "England", 2018))
list.add(ItemTitle("title3", 0))
//отправляем все данные в адаптер
adapter.swapData(list)
}
}
</listmarker>
Aquí es donde se inicializa nuestro RecyclerView
and ListAdapter
. Este es un código bastante repetitivo que muchos han encontrado. En el método, stubData
llené la lista con datos (como puede ver, datos con diferentes elementos) y pasé esta lista al adaptador. A continuación, iniciamos nuestra aplicación y deberíamos ver algo como esto en nuestra pantalla: Como puede ver, hay diferentes elementos en una lista, que era nuestro objetivo. PD: Olvidé mencionar el Extension
. Esto es lo que parece:
//это расширение для класса ViewGroup. Теперь мы можем по всему проекту использовать
//короткое inflate en lugar de длинного LayoutInflater.from(context).inflate
fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
Esto no es una clase, sino un archivo, por lo que no contiene ningún nombre. Pero ahora podemos usarlo en el adaptador simplemente inflate
en lugar de una estructura larga. Desafortunadamente, esto no está disponible para Java, así que escriba LayoutInflater.from(context).inflate
. Eso es todo. Hasta la próxima :) Enlace a GitHub
GO TO FULL VERSION