Android: Listas dinámicas usando RecyclerView
Tiempo de lectura: 6.69 minutos
En este tutorial aprenderás a crear listas dinámicas en Android, usando las clases RecyclerView y CardView.
¿Qué es RecyclerView?
La clase RecyclerView nos permite mostrar un listado (o bien una grilla) de elementos.
Lleva este nombre porque a medida que se renderizan los elementos de la lista, los elementos que dejan de observarse se reciclan para mostrar los elementos siguientes.
RecyclerView es una versión mejorada de la clase ListView, principalmente cuando el número de elementos es variable, y/o los datos cambian continuamente.
Es posible personalizar un ListView para lograr lo mismo, pero implicaría considerar varios detalles, a fin de conseguir el mismo rendimiento.
¿Qué es CardView?
Si bien un RecyclerView representa una lista de elementos, cada elemento debe tener una UI definida.
Al usar Material Design se suele usar la clase CardView para definir la apariencia de cada elemento de un listado, en la mayoría de los casos.
RecyclerView + CardView, ¿siempre juntos?
No es obligatorio que se usen en conjunto, pero es usual hacerlo.
¿En qué casos no se usarían en conjunto?
- Hay ocasiones en que se desea mostrar algo puntual. Entonces un elemento puede ser tan simple como un TextView, sin usar un CardView como contenedor.
- Es posible usar CardViews directamente sobre un Activity o Fragment, sin situarlos al interior de un RecyclerView.
Cuando no se requiere usar tarjetas con bordes y elevaciones, entonces se puede prescindir de la clase CardView.
Es posible usar cualquier otro componente visual para representar al layout de cada elemento del RecyclerView.
Lo que haremos
Para nuestro ejemplo vamos a tener un RecyclerView, donde cada elemento que estará representado por un CardView.
Generalmente todos los elementos de un RecyclerView tienen la misma apariencia.
Pero hay que tener en cuenta que no siempre todos los elementos de un RecyclerView son iguales.
Por ejemplo, si nuestra aplicación es una tienda online, podemos tener un listado de productos con sus imágenes, y entre ellos de forma aleatoria mostrar un código de descuento.
Si nuestra aplicación ayuda a nuestros usuarios a organizar sus tareas del día a día, podemos tener una frase inspiradora, y de vez en cuando algo más aburrido como un anuncio publicitario.
Un diagrama al rescate
Si comprendemos estos conceptos. Lo hemos comprendido todo sobre RecyclerViews.
Veamos uno por uno:
-
RecyclerView: Nuestro RecyclerView se va a "pintar" en función al LayoutManager que reciba como parámetro. También hará uso de un Adapter, que funcionará de acuerdo a un Dataset.
-
LayoutManager: Este "gestor del diseño" va a definir la disposición de los elementos. Es decir, si van formando una lista vertical u horizontal, si van formando una cuadrícula, u otra variante.
-
Adapter: El adaptador se encargará de adaptar el dataset a lo que finalmente verá el usuario. Es el encargado de traducir datos en UI.
- Dataset: Es el conjunto de datos que se espera mostrar en el RecyclerView. Se puede representar por un simple
array
de objetosString
; o ser algo más elaborado, como unArrayList
de objetos que presentan múltiples atributos.
Primero lo primero
Primero que todo vamos a añadir estas 2 dependencias a nuestro proyecto:
implementation 'com.android.support:cardview-v7:28.0.0'
implementation 'com.android.support:recyclerview-v7:28.0.0'
Y a continuación una explicación más detallada.
¿Cómo usar un RecyclerView?
Lo primero es definir un RecyclerView en nuestro Layout, es decir, en el XML de nuestro activity:
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Una vez hecho ello:
- obtenemos una referencia del RecyclerView,
- le asignamos un layout manager, y
- le asociamos un adapter
class MainActivity : AppCompatActivity() {
private val myDataSet = arrayOf(
"PHP",
"Javascript",
"Go",
"Python"
)
private val mAdapter by lazy {
MyAdapter(myDataSet)
}
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.my_activity);
recyclerView.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(baseContext)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = mAdapter
}
}
public class MyActivity extends Activity {
private RecyclerView mRecyclerView;
private MyAdapter mAdapter;
private static final String[] myDataSet = {
"PHP",
"Javascript",
"Go",
"Python"
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_activity);
mRecyclerView = findViewById(R.id.recyclerView);
// Esta línea mejora el rendimiento, si sabemos que el contenido
// no va a afectar al tamaño del RecyclerView
mRecyclerView.setHasFixedSize(true);
// Nuestro RecyclerView usará un linear layout manager
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
mRecyclerView.setLayoutManager(layoutManager);
// Asociamos un adapter (ver más adelante cómo definirlo)
mAdapter = new MyAdapter(myDataSet);
mRecyclerView.setAdapter(mAdapter);
}
// ...
}
¿Cómo definir un adapter?
Un adapter:
-
contiene una clase interna
ViewHolder
, que permite obtener referencias de los componentes visuales (views
) de cada elemento de la lista, -
presenta un constructor y/o métodos para gestionar el Data Set (añadir, editar o eliminar elementos),
-
contiene un método
onCreateViewHolder
que infla el layout (archivo xml) que representa a nuestros elementos, y devuelve una instancia de la clase ViewHolder que antes definimos; -
contiene un método
onBindViewHolder
que enlaza nuestra data con cada ViewHolder, y - contiene un método
getItemCount
que devuelve un entero indicando la cantidad de elementos a mostrar en el RecyclerView.
class MyAdapter (private val mDataSet: Array)
: RecyclerView.Adapter() {
// En este ejemplo cada elemento consta solo de un nombre
class ViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView)
// El layout manager invoca este método para renderizar cada elemento
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.item_text_view, parent, false) as TextView
// Aquí podemos definir tamaños, márgenes, paddings, etc
return ViewHolder(v)
}
// Este método asigna valores para cada elemento de la lista
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = mDataSet[position]
}
// Cantidad de elementos del RecyclerView
// Puede ser más complejo (por ejm, si implementamos filtros o búsquedas)
override fun getItemCount() = mDataSet.size
}
public class MyAdapter extends RecyclerView.Adapter {
private String[] mDataSet;
// Obtener referencias de los componentes visuales para cada elemento
// Es decir, referencias de los EditText, TextViews, Buttons
public static class ViewHolder extends RecyclerView.ViewHolder {
// en este ejemplo cada elemento consta solo de un título
public TextView textView;
public ViewHolder(TextView tv) {
super(v);
textView = tv;
}
}
// Este es nuestro constructor (puede variar según lo que queremos mostrar)
public MyAdapter(String[] myDataSet) {
mDataSet = myDataSet;
}
// El layout manager invoca este método
// para renderizar cada elemento del RecyclerView
@Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
// Creamos una nueva vista
TextView v = (TextView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_text_view, parent, false);
// Aquí podemos definir tamaños, márgenes, paddings, etc
ViewHolder vh = new ViewHolder(v);
return vh;
}
// Este método asigna valores para cada elemento de la lista
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// - obtenemos un elemento del dataset según su posición
// - reemplazamos el contenido usando tales datos
holder.mTextView.setText(mDataSet[position]);
}
// Método que define la cantidad de elementos del RecyclerView
// Puede ser más complejo (por ejem, si implementamos filtros o búsquedas)
@Override
public int getItemCount() {
return mDataSet.length;
}
}
DataSet
Como puedes observar, dentro del adapter
tenemos un atributo llamado mDataSet
.
En este caso, la fuente de datos que espera recibir el adaptador es solo un arreglo de objetos String
.
Pero esta fuente de datos puede ser más compleja, según se requiera.
Por ejemplo, si queremos mostrar información de usuarios es posible que el atributo mDataSet
se defina como un ArrayList
de objetos User
(o de la entidad que deseamos mostrar en nuestra lista).
ViewHolder
En el método onCreateViewHolder
hacemos uso de la clase LayoutInflater
para "inflar" un layout XML.
Para este simple ejemplo, donde listamos lenguages de programación, el layout se representa solo por un TextView
.
Lo podemos definir como item_text_view.xml
dentro de la carpeta res\layout
:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tvName"
android:padding="12dp"
android:layout_height="wrap_content"
android:layout_width="match_parent" />
Pero este layout, como ya comentamos, también puede constituirse de varios componentes.
Por ejemplo de un CardView
y contener varios Button
y TextView
.
En resumen, debemos definir un nuevo recurso XML con la apariencia que tendrán nuestros elementos, y luego usar ese layout en el método onCreateViewHolder
.
Aquí un ejemplo de un layout XML que representa los datos de una entidad "Informe" a través de un CardView:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardUseCompatPadding="true"
card_view:cardElevation="4dp"
card_view:cardCornerRadius="3dp"
android:layout_margin="6dp">
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp"
android:gravity="center">
<TextView
android:id="@+id/tvInformId"
android:textColor="@color/colorPrimary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="INFORME 777" />
<TextView
android:id="@+id/tvUserName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance"
android:text="Juan Ramos" />
<TextView
android:id="@+id/tvCreatedAt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Creado el día 01/02/2017"/>
<TextView
android:id="@+id/tvFromDate"
android:textColor="@color/colorPrimaryDark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
android:text="Desde 17/03/2017" />
<TextView
android:id="@+id/tvToDate"
android:textColor="@color/colorPrimaryDark"
android:layout_width="wrap_content"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
android:layout_height="wrap_content"
android:text="Hasta 17/07/2017" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnGoToReports"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Ver reportes" />
<Button
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="Editar informe"
android:id="@+id/btnEditInform" />
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
Y este es el resultado:
Ejecutar una acción por cada elemento
Como bien sabes, al programar, hay muchas maneras de lograr lo mismo.
Si deseas que al hacer clic sobre un elemento se ejecute una acción, puedes asociar un listener directamente sobre el elemento.
O bien definir una interfaz, con la intención de ejecutar la acción desde tu Activity o Fragment.
La interfaz se puede definir al interior de la clase Adapter:
interface OnItemSelectedListener {
fun onItemSelected(position: Int)
}
Y entonces tu Activity o Fragment deberá implementar dicha interfaz, y ser enviado como parámetro al constructor del Adapter.
La interfaz asegurará que se implemente el método onItemSelected
, que será ejecutado al hacer clic sobre un elemento.
Lo cierto es que:
- este método podría de llamarse de la forma que tú prefieras,
- como parámetro puedes enviar un
Int
con la posición, pero no es el único camino; - por ejemplo, podrías enviar un objeto, tomándolo del dataSet, a partir de su posición.
Sugerencias finales
Si tienes dudas, realmente te recomiendo inscribirte a mi curso de Laravel y Android, donde vemos paso a paso cómo desarrollar una aplicación para reserva de citas médicas.
En el curso, el registro de una cita médica consta de varios datos, así que también vemos cómo hacer un registro en varios pasos; y nuestros RecyclerView presentan elementos donde cada uno es un CardView con animaciones (para expandir o contraer la información).
Otra buena alternativa es adquirir el código fuente, de este otro proyecto sobre diagnósticos y tratamiento de enfermedades.
? ¡Éxitos en todo!