Android: Listas dinámicas usando RecyclerView

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

Diagrama sobre cómo funciona un RecyclerView

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 objetos String; o ser algo más elaborado, como un ArrayList 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:

Así se ve un CardView

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!

# android

Logo de Programación y más

Comparte este post si te fue de ayuda 🙂.

Regístrate

Accede a todos los cursos, y resuelve todas tus dudas.

Cursos Recomendados

Imagen para el curso Laravel y Android

Laravel y Android

Curso intensivo. Incluye el desarrollo de una API, su consumo, y autenticación vía JWT. También vemos Kotlin desde 0.

Iniciar curso
Imagen para el curso Docker y Microservicios

Docker y Microservicios

Aprende por qué es importante y cómo funciona Docker, con este nuevo curso práctico!

Iniciar curso
Imagen para el curso Aprende Python

Aprende Python

Desarrolla tu primer Chatbot para Facebook Messenger sobre Google Cloud, y aprende Python en el camino!

Iniciar curso

Espera un momento ...

¿Te gustaría llevar mi curso de Laravel, gratis?

Sólo debes ingresar tus datos: