El RecyclerView
en Android es un ViewGroup
flexible que permite mostrar un conjunto de datos con capacidad de scroll. Su uso más común es la presentación de listas de ítems al usuario.
A diferencia del ListView
, el RecyclerView reúsa los views de los ítems en su jerarquía, en vez de destruirlos cuando el desplazamiento alcanza los límites de la pantalla. Lo cual se refleja en mejora de rendimiento y por lo tanto menor consumo de tu App.
Ejemplo De RecyclerView En Android
En este tutorial aprenderás a implementar y configurar un RecyclerView
a partir de las clases e interfaces proveídas por la librería que lo contiene. A continuación se muestra una imagen de varios ejemplos que construirás:
La idea es crear diferentes layouts (lista, grilla y grilla escalonada) del RecyclerView
para mostrar una lista de artículos sobre estudios.
Aunque el objetivo es que llegues a este resultado con la explicación dada, puedes descargar el proyecto Android Studio desde el siguiente enlace por si te pierdes en detalles que en ocasiones son obviados:
RecyclerView En Android
Las siguientes son las clases fundamentales que actúan como participantes en la estructura de creación de una lista de elementos:
- Datos: Determinan la colección que se quiere mostrar sobre los ítems (arrays, listas, sets, etc.).
RecyclerView
: Es el contenedor donde se mostraran el resultado final, es decir, los views producidos por el proceso de inflado y población de datos. Usaremos instancias de esteViewGroup
en los layouts de Actividades y fragmentos como normalmente lo hacemos.RecyclerView.ViewHolder
: Contiene la referencia del view creado para un ítem y la información de su lugar en elRecyclerView
. Esto te permitirá vincular directamente tu información sobre los widgets del ítem.RecyclerView.Adapter
: Colabora con elViewHolder
para convertir una colección de datos en una lista de views que serán añadidos alRecyclerView
y enlazados con dichos datos.RecyclerView.LayoutManager
: Es el responsable de medir y posicionar los ítems dentro delRecyclerView
. Además determinar cuando los ítems no son visible para el usuario. Existen varias implementaciones de esta clase para permitirnos crear listas, grillas o estructuras escalonadas.
La siguiente ilustración muestra de forma general la anterior estructura:
Implementar RecyclerView
Basado en los conceptos previos, procedamos a realizar la implementación de nuestro ejemplo a partir de la siguiente receta.
1. Agregar Dependencia Gradle
Hoy en día la creación de nuevos proyectos en Android Studio trae por defecto la dependencia Gradle de la librería RecyclerView
.
Pero si tu proyecto tiene cierto tiempo de creado o no fue creado por ti, entonces asegúrate de incluirla con la última versión en el archivo build.gradle
del módulo:
dependencies {
implementation "androidx.recyclerview:recyclerview:ultima-version"
}
2. Definir Datos A Mostrar
El primer componente a proveer es el tipo de datos que deseas mostrar en el RecyclerView
. Este puede ser un tipo básico como un String
o un tipo complejo como la instancia de una clase.
Para nuestro ejemplo usaremos una clase de datos que representará a los artículos del aplicativo. Por lo que agregamos el archivo Article.kt
al proyecto e implementamos las propiedades de: ID, título, descripción y la imagen destacada:
data class Article(
val id: Int,
val title: String,
val description: String,
val featuredImage: Int
)
Por simplicidad, le añadiremos un objeto compañero para exponer una lista prefabricada de Article
en memoria.
companion object {
val data
get() = listOf(
Article(
1,
"10 Tips Para Estudiar",
"Descubre cómo aumentar tu productividad al estudiar",
R.drawable.learning1
),
Article(
2,
"Guía para escribir tu primer cuento",
"Incursiona en el mundo de la narración infantil",
R.drawable.learning2
),
Article(
3,
"Optimizar trabajos grupales",
"Aplica estas estrategias para mejorar tus trabajos en grupo",
R.drawable.learning3
),
Article(
4,
"Libros que nunca habías escuchado",
"Te presentamos la lista de los libros más raros",
R.drawable.learning4
),
Article(
5,
"Cómo mejorar en la universidad",
"En este artículo una actitud adecuada para la U",
R.drawable.learning5
),
Article(
6,
"40 buscadores de artículos científicos",
"Descubre los buscadores más importantes para cada área del conocimiento",
R.drawable.learning6
),
Article(
7,
"Pautas para escribir un ensayo",
"Karla te explica un marco de trabajo para hace ensayos",
R.drawable.learning7
),
Article(
8,
"Crear un ambiente de estudio para llegar a \"la zona\"",
"Aprende a modificar tu entorno para sacar el máximo beneficio de tu mente",
R.drawable.learning8
),
Article(
9,
"Estudiar 80 horas semanales",
"Como Carlos logró estudiar 80 horas sin agotarse",
R.drawable.learning9
),
Article(
10,
"Lo que tu tutor de tesis no te dice",
"Consejos para terminar trabajos de grado rápido",
R.drawable.learning10
)
)
}
3. Añadir RecyclerView Al Layout
El paso siguiente es añadir el RecyclerView
al layout de la actividad o fragmento donde será mostrada la lista.
Así que abre tu layout y desde el editor de Android Studio arrastra un ejemplar desde la paleta de views:
Si pasas a la pestaña de código podrás ver que se ha generado una etiqueta <androidx.recyclerview.widget.RecyclerView>
en tu layout.
Por ejemplo, nuestro archivo article_item.xml
tendría la siguiente definición XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingVertical="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Ya que es necesario definir el LayoutManager
que usará el RecyclerView
, asignamos al atributo app:layoutManager
el nombre completamente calificado de la clase LinearLayoutManager
.
Este descendiente organiza a los ítems para proyectarlos de forma lineal sobre el RecyclerView.
4. Crear Layout Para Los Ítems
Nuestro próximo material es el layout para los ítems puesto que el adaptador requiere su uso posteriormente.
En concreto diseñaremos un ítem de dos líneas con una imagen en la parte izquierda para presentar a cada artículo. Es decir, dos TextViews
junto a un ImageView
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="88dp">
<ImageView
android:id="@+id/featured_image"
android:layout_width="100dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/article_image_desc"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/article_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/featured_image"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/article_description"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:ellipsize="end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/article_title"
app:layout_constraintTop_toBottomOf="@+id/article_title"
app:layout_constraintVertical_bias="0.0"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>
El Atributo tools:listitem
Puedes previsualizar el aspecto que tendrá tu lista en la pantalla del dispositivo usando el atributo tools:listitem
sobre el RecyclerView
.
Solo asigna la referencia del recurso del layout del ítem y pasa a la pestaña Split o Design para renderizar el inflado de los artículos:
tools:listitem="@layout/article_item"
5. Crear Adaptador Del RecyclerView
Ahora procedemos a crear una implementacion del adaptador que usará el RecyclerView
, aplicando una herencia desde RecyclerView.Adapter
.
Al mismo tiempo añadimos una clase anidada que extienda de RecyclerView.ViewHolder
, ya que el adaptador va ligado a esta declaración. De manera que, añade una nueva clase llamada ArticleAdapter
:
class ArticleAdapter: RecyclerView.Adapter<ArticleAdapter.ArticleViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
TODO("Not yet implemented")
}
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
TODO("Not yet implemented")
}
override fun getItemCount(): Int {
TODO("Not yet implemented")
}
class ArticleViewHolder(view: View) : RecyclerView.ViewHolder(view) {
}
}
Luego declararemos una lista de elementos Article
como propiedad mutable del adaptador con el objetivo de establecer un origen de datos:
var articles = listOf<Article>()
set(value) {
field = value
notifyDataSetChanged()
}
La lista articles
comienza vacía, pero personalizamos su set()
para actualizar el campo de respaldo normalmente y luego invocar a notifiyDataSetChanged()
.
Este método le notifica al adaptador que los datos cambiaron, por lo que se debe redibujar a todos los elementos en la lista.
Implementar onCreateViewHolder()
Este método es llamado por el RecyclerView
cuando requiere crear un ViewHolder
para un ítem. Tu trabajo es inflar el layout del ítem, o crear programáticamente su view, para pasarlo como parámetro a una nueva instancia del view holder.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
val view = LayoutInflater
.from(parent.context)
.inflate(R.layout.item_list, parent, false)
return ArticleViewHolder(view)
}
Implementar onBindViewHolder()
El RecyclerView llama a este método para vincular los datos al ViewHolder
en una posición dada. Tu tarea es hacer la asignación del modelo a cada view de la jerarquía del ítem.
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = articles[position]
holder.bind(article)
}
class ArticleViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.article_title)
val description: TextView = view.findViewById(R.id.article_description)
val featuredImage: ImageView = view.findViewById(R.id.featured_image)
fun bind(article: Article) {
title.text = article.title
description.text = article.description
featuredImage.setImageResource(article.featuredImage)
}
}
Aunque podríamos hacer la asignación de valores desde onBindViewHolder()
, podemos encapsular esta acción en un nuevo método bind()
donde pasamos el artículo en la posición actual.
Implementar getItemCount()
Le permite saber al RecyclerView
cuál es el tamaño actual de tu colección de datos. Claramente aquí retornas un entero con la propiedad de tamaño de tu conjunto.
override fun getItemCount() = articles.size
6. Asignar Adaptador Al RecyclerView
Terminamos la implementación tomando la referencia del RecyclerView
y diciéndole que adaptador será el encargado de hacer la transformación del conjunto de datos.
En términos de código Kotlin esto quiere decir:
- Vamos a nuestra actividad o fragmento y tomamos la referencia del RecyclerView
- Creamos una nueva instancia de
ArticleAdapter
- Asignamos el adaptador a la propiedad mutable
RecyclerView.adapter
- Actualizamos los datos del adaptador con la lista
Article.data
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.articles)
val articleList: RecyclerView = findViewById(R.id.list) // (1)
val articleAdapter = ArticleAdapter() // (2)
articleList.adapter = articleAdapter // (3)
articleAdapter.articles = Article.data // (4)
}
}
También es posible asignar el objeto LayouManager
en tiempo de ejecución si así lo deseas:
articleList.layoutManager = LinearLayoutManager(this) // (3.1)
Optimizar RecyclerView Con Tamaño Fijo
Si la lista que deseas mostrar tiene una cantidad de ítems fija a lo largo del caso de uso, puedes usar el método setHasFixedSize()
con true
para acotar el comportamiento del RecyclerView
y mejorar el rendimiento de dibujado:
articleList.setHasFixedSize(true) // (3.2)
Manejar Eventos De Click En Ítems
Tenemos presente que hacer los ítems clickables es de gran importancia para que el usuario interactúe con los datos en nuestras Apps.
Por simplicidad, nuestro ejemplo solo mostrará un Toast
al momento de hacer click en un ítem. No obstante, la forma de conseguir esa pequeña acción es exactamente igual para tareas más complejas como navegar a otras pantallas.
¿Cómo escuchar clicks de un ítem?
1. La solución consiste en asignarle una escucha View.OnClickListener
al view almacenado por ArticleViewHolder
. Al detectar el click se ejecuta un tipo función (Article) -> Unit
que pasamos en su constructor:
class ArticleViewHolder(view: View, val onClick: (Article) -> Unit) :
RecyclerView.ViewHolder(view) {
//..
private var currentArticle: Article? = null
init {
view.setOnClickListener {
currentArticle?.let {
onClick(it)
}
}
}
fun bind(article: Article) {
currentArticle = article
//..
}
}
2. Luego puedes elegir manejar el evento de click al interior del ViewHolder o desde el exterior según tu conveniencia. En nuestro caso lo haremos desde el exterior.
Esto podemos lograrlo fácilmente pasando una función del tipo (Article) -> Unit
a través del constructor del adaptador:
class ArticleAdapter(private val onClick: (Article) -> Unit)
En el caso de que usaras un ViewModel, lo correcto sería pasarle el manejo del evento, ya que esa es su responsabilidad.
3. En seguida, vamos a la creación del adaptador en la actividad y pasamos como argumento una función lambda que represente el consumo del tipo función:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//..
val articleAdapter = ArticleAdapter { article -> onItemClick(article) }
//..
}
private fun onItemClick(article: Article) {
Toast.makeText(this, article.title, Toast.LENGTH_SHORT).show()
}
De esta forma, cada que se realice un click en un ítem, tendremos a mano el modelo de datos asociado para realizar las acciones correspondientes.
Aplicar Efecto Ripple A Los Ítems
Si deseas presentar un efecto de superficie al clickear los ítems del RecyclerView usa el atributo android:background="?attr/selectableItemBackground"
sobre el nodo raíz del layout del mismo:
Concretamente sería sobre el ConstraintLayout
que sostiene la jerarquía de cada artículo:
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:padding="16dp"
android:background="?attr/selectableItemBackground"
android:layout_height="88dp">
Optimizar Listas Largas
En nuestra App de artículos de estudio usamos pocos ítems en la lista para simplificar el caso de uso en el tutorial, por eso llamamos al método notifyDataSetChanged()
con tanta comodidad sobre el set()
de articles
del adaptador.
Sin embargo, realizar notificaciones desde el adaptador cuando existen grandes cantidades de elementos, comprometería el rendimiento y fluidez de la interfaz.
Veamos como mejorar esta situación.
1. Crear Callback A Partir De DiffUtil
Es justo aquí donde entra la clase DiffUtil
para ejecutar la comparación entre el conjunto de datos actual del adaptador versus el nuevo estado.
Esto permitirá optimizar la cantidad de notificaciones que se realizan y mejora el rendimiento en casos donde existen grandes cantidades de ítems.
¿Cómo usas esta clase?
Para obtener los comportamientos para la optimización creamos una clase que implemente a DiffUtil.ItemCallback<T>
, donde el argumento de tipo sea nuestro modelo (Article
):
class ArticleDiffCallback : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
Los métodos areItemsTheSame()
y areContentsTheSame()
ejecutan una comparación de igualdad de la identidad y contenido entre el ítem (oldItem
) existente y el nuevo (newItem
).
Cabe destacar que al marcar como data class
a Article
, el operador de igualdad comparará el contenido propiedad a propiedad como es necesitado en areContentsTheSame()
.
2. Extender Adaptador De ListAdapter
La librería nos otorga una clase llamada ListAdapter
que implementa internamente todas las notificaciones de cambio del conjunto de datos de forma eficiente.
Partiendo con esta ventaja, tan solo heredamos a ArticleAdapter
desde ListAdapter<T, VH>
y luego pasamos una instancia de la callback al constructor:
class ArticleAdapter(private val onClick: (Article) -> Unit) :
ListAdapter<Article, ArticleAdapter.ArticleViewHolder>(ArticleDiffCallback())
El parámetro de tipo T
se refiere al tipo que usaran las listas a comparar. Y VH
es el parámetro para el tipo de ViewHolder
asociado.
Gracias a este cambio ocurren tres cosas:
- Removemos a la propiedad
articles
ya queListAdapter
se encarga de manejar las listas internamente. - Reemplazamos a
articles[position]
porListAdapter.getItem()
para obtener el item actual enonBindViewHolder()
- Removemos el método
getItemCount()
porque es manejado internamente
De esta forma nuestro adaptador quedaría listo para optimizar las notificaciones a partir del cálculo de diferencias.
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = getItem(position)
holder.bind(article)
}
3. Actualizar Lista
Para terminar esta implementación debes cambiar la forma de actualizar la lista de artículos.
La clase ListAdapter
también nos otorga el método submitList()
para enterar al adaptador de una nueva actualización de los datos.
Así que vamos a la creación del adaptador y reemplazamos la actualización anterior por la llamada de este método:
articleAdapter.submitList(Article.data)
Modificar El Layout
Además de el LinearLayoutManager
usado para ítems de lista también existen dos estructuras más prefabricadas: GridLayoutManager
y StaggeredLayoutManager
.
Veamos como modificar en tiempo de ejecución nuestra lista actual para que tome estas formas.
Crear Grilla Con GridLayouManager
Afortunadamente podemos reutilizar todo lo que hicimos hasta el momento para implementar una grilla desde el mismo modelo de datos.
¿Cuál es la única diferencia?
La forma en que se comporta la organización de los ítems. El GridLayoutManager
usa una propiedad llamada span para la cantidad de columnas o filas en el layout:
Obviamente al cambiar los espacios y geometría de los ítems, necesitaremos actualizar el layout del ítem para mostrar una presentación acorde:
Ubicaremos la imagen en la parte superior de cada ítem y pasaremos el título a la parte inferior. La idea es conseguir una forma de recuadros coleccionables:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="194dp"
android:background="?attr/selectableItemBackground"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical"
tools:layout_width="190dp">
<ImageView
android:id="@+id/featured_image"
android:layout_width="match_parent"
android:layout_height="140dp"
android:contentDescription="@string/article_image_desc"
android:scaleType="centerCrop"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/article_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="2"
android:paddingHorizontal="16dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
tools:text="@tools:sample/lorem" />
</LinearLayout>
Luego asignamos una instancia GridLayoutManager
con 2 columnas en la configuración del RecyclerView
. Recuerda que puedes usar apply()
para empaquetar en un mismo contexto la configuración:
val gutter = resources.getDimensionPixelSize(R.dimen.grid_gutter)
articleList.apply {
adapter = articleAdapter
layoutManager = GridLayoutManager(this@MainActivity, 2)
addItemDecoration(GutterDecoration(gutter))
}
Debido a que la grilla sin espacios se ve mal, usaremos un decorador que añada canales entre los bordes de cada ítem. El código Kotlin de decorador será el siguiente:
class GutterDecoration(private val gutter: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect, view: View,
parent: RecyclerView, state: RecyclerView.State
) {
outRect.left = gutter / 2
outRect.right = gutter / 2
outRect.bottom = gutter
}
}
La variable gutter
recibe la conversión en pixeles a partir del recurso R.dimen.grid_gutter
(16dp) a través del método getDimensionsPixelSize()
.
Ocupar Span Completo
Puede presentarse la situación en la que desees que un item ocupe un span completo de la grilla como se ve en la siguiente captura:
Para hacerlo efectivo, usaremos la clase SpanSizeLookup
, la cual define cuantos spans ocupa un ítem en el layout. Obviamente como hemos experimentado su valor por defecto es 1.
Sin embargo, como deseamos que el primer elemento ocupe 2 espacios, entonces debemos crear una instancia de SpanSizeLookup
que aplique esta condición.
Así que ve a la creación del adaptador e intervén la creación del GridLayoutManager
para asignarle un nuevo objeto anónimo de la clase en la propiedad spanSizeLookup
:
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = when (position) {
0 -> 2
else -> 1
}
}
La expresión when
nos permite formular para la posición 0 el tamaño 2 y para los demás el valor por defecto 1.
Crear Grilla Escalonada Con StaggereedLayoutManager
Como último layout tenemos la formación con grilla escalonada representada por el StaggeredLayoutManager
.
Este administrador permite añadir espacios entre los bordes de los ítems en la grilla que puedes aplicar de forma independiente o uniforme entre toda la distribución. Por ejemplo:
A causa de que ya tenemos un layout para ítems de grilla que podemos reutilizar, entonces solo quedaría cambiar el LayoutManager
en tiempo de ejecución desde la actividad:
val staggeredManager =
StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
val gutter = resources.getDimensionPixelSize(R.dimen.grid_gutter)
articleList.apply {
adapter = articleAdapter
layoutManager = staggeredManager
addItemDecoration(StaggeredGutterDecoration(gutter))
}
Cabe aclarar que el StaggeredGridLayoutManager
usa la capacidad de adaptación del layout de tus ítems de grilla. Si fijas las alturas en un tamaño determinado, no verás introducción de espacios.
En nuestro ejemplo modificamos la altura del ítem desde el adaptador a partir de las posiciones:
fun bind(article: Article) {
currentArticle = article
title.text = article.title
featuredImage.setImageResource(article.featuredImage)
itemView.updateLayoutParams {
val res = if (isDoubleHeightPosition(adapterPosition))
R.dimen.image_height1
else
R.dimen.image_height2
height = itemView.resources.getDimensionPixelSize(res)
}
}
El método updateLayoutParams()
nos permite cambiar la propiedad height
de itemView
en el ArticleViewHolder
.
Donde el método de utilidad isDoubleHeightPosition()
calcula si la posición actual es un indice para introducir elementos con el doble te tamaño:
object AsymmetricCalculator {
fun isDoubleHeightPosition(position: Int): Boolean {
val distanceLeftToRight = 4
val distanceRightToLeft = 2
val positions = generateSequence(Pair(0, distanceLeftToRight)) {
val newFirst = it.second + distanceRightToLeft
val newSecond = newFirst + distanceLeftToRight
Pair(newFirst, newSecond)
}
return positions.flatMap { it.toList() }.take(7).any { it == position }
}
}
Ya que los índices no están distribuídos linealmente, generamos una secuencia de Kotlin para contar 4 elementos de izquierda a derecha para introducir ítems con el doble de tamaño.
Y contamos 2 posiciones para insertar ítems con doble tamaño en el canal izquierdo. Luego aplanamos los pares con flatMap()
, tomamos 7 elementos con take()
y luego determinamos con any()
si el índice entrante hace parte de ellos.
Añadir Ítems Con Diferente Layout
En ocasiones necesitarás incluir ítems a la lista con diferentes diseños en posiciones particulares del RecyclerView
. Ítems especiales tales como cabeceras, anuncios, elementos destacados, etc.:
Uno de nuestros ejemplos muestra una cabecera inicial con el título «Artículos recientes».
Aunque podríamos ponerlo como un view externo al RecyclerView
, vale la pena ver cómo integrarlo con el fin de pensar en casos más avanzados cuando lo requieras:
1. Lo primero que haremos será crear una clase sellada que contenga la representación de las cabeceras y los artículos. Este diseño permitirá facilitar el casting al interior del adaptador:
sealed class DataItem {
abstract val id: Int
object HeaderItem : DataItem() {
override val id = Int.MIN_VALUE
}
data class ArticleItem(val article: Article) : DataItem() {
override val id = article.id
}
}
Como ves, HeaderItem
es una definición de objeto que expresa la existencia de una sola cabecera y ArticleItem
envuelve la instancia de Article
para sostener su funcionalidad en esta jerarquía de clases.
La propiedad abstracta id será de utilidad para realizar las comparaciones con ArticleDiffCallback
.
2. El paso siguiente es sobrescribir el método getItemViewType()
en el adaptador y retornar enteros que representen los diferentes tipos de items que existirán:
private val HEADER_TYPE = 0
private val ITEM_TYPE = 1
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataItem.HeaderItem -> HEADER_TYPE
is DataItem.ArticleItem -> ITEM_TYPE
}
}
El valor retornado por getItemViewType()
es recibido en onCreateViewHolder()
, lo que significa que se podrán crear diferentes view holders asociados al tipo.
3. Ahora bien, crea un nuevo ViewHolder para la cabecera que infle el layout correspondiente:
class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
companion object {
fun create(parent: ViewGroup): HeaderViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.header, parent, false)
return HeaderViewHolder(view)
}
}
}
Como solo necesitas texto, el layout puede ser un TextView
sin más:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="@string/lastest_articles"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" />
4. Luego actualizamos onCreatViewHolder()
y onBindViewHolder()
para dirigir el curso dependiendo del tipo:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
HEADER_TYPE -> HeaderViewHolder.create(parent)
ITEM_TYPE -> ArticleViewHolder.create(parent, onClick)
else -> throw IllegalArgumentException("Tipo de ítem desconocido")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ArticleViewHolder -> {
val item = getItem(position) as DataItem.ArticleItem
holder.bind(item)
}
}
}
El uso de la expresión when
es fundamental para dirigir el curso de inflado y de binding de datos dependiendo de viewType
y del casting realizado a holder
.
5. Claro está que al usar a DataItem
como modelo del adaptador, ArticleAdapter
debe usarlo como parámetro de tipo al igual que ArticleDiffCallbak
:
class ArticleAdapter(private val onClick: (Article) -> Unit) :
ListAdapter<DataItem, RecyclerView.ViewHolder>(ArticleDiffCallback()) {
//..
}
class ArticleDiffCallback : DiffUtil.ItemCallback<DataItem>() {
override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem == newItem
}
}
Finalmente ve a la actividad y crea una lista cuyo primer elemento sea del tipo HeaderItem
; y el resto un mapeo hacia ArticleItem
desde los datos en Article
:
val list = listOf(DataItem.HeaderItem) + Article.data.map { DataItem.ArticleItem(it) }
articleAdapter.submitList(list)