BasicRecyclerView

En esta entrada vamos a implementar una activity que integre un RecyclerView (que es una evolución del ListView). Este RecyclerView cargará simplemente una lista de nombres de autores que se puede obtener de diversas fuentes, en este caso de un List<String> que definimos en el código. El RecyclerView estará incialmente vacío hasta que pulsemos un botón para cargar su contenido.

Esta es la estructura de un RecyclerView (RV). Faltaría añadir dentro del Adapter un ViewHolder como veremos. Básicamente es como un ListView que emplea un ViewHolder pero más avanzado.

En este ejemplo hemos definido una vista simple para cada uno de los items del RV, pero podría ser más compleja. También hemos añadido código para incluir un Listener para capturar los clicks en los elementos del Layout de cada item.

  1. Por un lado tenemos que añadir código a la Activity que incluya el RecyclerView
  2. Por otro tenemos que definir una nueva clase para nuestro Adapter.

Vamos a implementar un ejemplo sencillo como se ve en la imagen, donde ya están cargados los items en el RV, porque hemos pulsado el botón Load. Nuestro RecyclerView tiene 10 layouts (items) muy sencillos, básicamente un TextView (pero que está dentro de un ConstraintLayout). Como hemos dicho podríamos haber definido un layout todo lo complejo que queramos, se tratará como un item normal.

Una visión de su arquitectura es esta:

El RecyclerView es una vista que muestra una lista de layouts, uno por item, conforme el usuario mueve la lista, hay elementos que aparecen y otros que desaparecen de la lista. Estos layouts que desaparecen de la vista son reutilizados, gracias a su arquitectura, para no consumir memoria en exceso si la lista es muy grande. Un esquema de esto es el siguiente:

De los layouts de los items que quedan fuera del área visible se destruye su contenido pero el layout se guarda para ser reciclado.

Mediante el ViewHolder definimos la vista del layout, el sistema llama a dos callbacks del adaptador, primero a onCreateViewHolder donde se crean las vistas de los items, una vez creadas luego, conforme va necesitando poner datos nuevos en cada una de las vistas, llama al callback onBindViewHolder donde colocamos los datos en las vistas internas el layout del item (normalmente un único TextView como en este ejemplo)

Creando un Adaptador específico

Vamos a empezar por el punto 2, es decir crear una clase adaptador propia para que cargue los datos. Estos datos se le dan al adaptador, y en este ejemplo, como hemos dicho, los datos están picados a mano en el código y se le pasan, pero podrían estar en un fichero de texto, o en un fichero xml, o en una base de datos o venir en un JSON a través de una conexión con un servidor.

Tenemos que tener claro primero varios temas.

  • Nuestro adaptador tendrá como miembro de la clase un conjunto de datos, que serán los que le lleguen por el constructor.
  • Nuestro adaptador tendrá un inflador que se encargará de “inflar” el layout específico que tendremos para cada item del RV.
  • Nuestro adaptador tendrá una clase interna para el ViewHolder, que será llamado por android para gestionar la reutilización (Recycle) y pre-carga de aquellos items que se quedan fuera de la zona visible del RV. Además este ViewHolder será el receptor primario de los eventos (callbacks onClick()) que Android lanza cuando el usuario hace click en un elemento (item) del RV. Por tanto implementará un View.OnClickListener. Lo veremos con detalle…
  • Nuestro adaptador tendrá un constructor que recibirá un Context (la Activity que inluye el RV) y los datos con los que va a trabajar.
  • Nuestro adaptador extenderá (heredará) de la clase RecyclerView.Adapter<Nuestro ViewHolder>, es decir, heredadrá de un Adapter para RecyclerViews que tiene un ViewHolder que definimos nosotros (el que acabamos de hablar). Por tanto nos va a obligar a sobrecargar una serie de métodos.

Vamos a empezar por este último punto.

Sobrecargando los métodos necesarios

Estos métodos son (nos ofrece el editor que si los queremos crear, con lo que no es necesario saberselos de memoria)

  • public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
  • public void onBindViewHolder(MyViewHolder holder, int position)
  • public int getItemCount()

getItemCount : Lo necesita Android para saber cuantos elementos estamos gestionando para calcular cuantos quedan fuera de la zona visible y reutilizar los viewHolders.

onCreateViewHolder : Lo llama Android cuando tiene que crear un nuevo item, nuestro ViewHolder sabe de donde inflarlo, devuelve uno de nuestros objetos ViewHolder con el layout correspondiente inflado, para lo que llama al constructor de nuestro ViewHolder.

onBindViewHolder : Lo llama Android para que vinculemos los datos que gestionamos con los Views que componen nuestro layout para cada item. (En nuestro ejemplo sólo un TextView, pero podrían ser más cosas)

Veamos ya el código completo de nuestra clase Adapter, que hemos llamado MyRVAdapter, y así vemos lo que hemos comentado hasta ahora y seguiremos comentando a la vista del código, iré comentando bloques de este código poco a poco.

package com.miguel.amm.basicrecyclerview;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.List;

public class MyRVAdapter extends RecyclerView.Adapter<MyRVAdapter.MyViewHolder> {

    private List<String> autores;
    private LayoutInflater mInflater;
    private ItemClickListener mClickListener;

    // La actividad que contenga el RecyclerViewer va a tener que implementar esta interfaz
    // para recibir los clicks en los items de nuestro recycler view.
    public interface ItemClickListener {
        void onRVItemClick(View view, int position);
    }

    // El Activity que incluya el Recycler View que utilice este adapter llamará a este metodo para indicar que es el listener.
    void setClickListener(ItemClickListener itemClickListener) {
        this.mClickListener = itemClickListener;
    }

    // Se llamara desde el ItemClickListener implementado en la Actividad contenedora
    public String getItem(int position) {
        return autores.get(position);
    }

    // Constructor, los datos los recibimos en el constructor.
    public MyRVAdapter(Context context, List<String> autores) {
        super();
        this.mInflater = LayoutInflater.from(context);
        this.autores = autores;
    }

    // ----------METODOS QUE HAY QUE SOBRECARGAR DE LA CLASE RecyclerView.Adapter<> ----------
    // inflates the row layout from xml when needed
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = mInflater.inflate(R.layout.autor_item, parent, false);
        return new MyViewHolder(view);
    }

    // Binds (vincula) los datos al Textview para cada item
    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        String autor = autores.get(position);
        holder.tvAutor.setText(autor);
    }

    // Número total de items
    @Override
    public int getItemCount() {
        return autores.size();
    }
    // ---------------------------------------------------------------------------------------


    // --------- IMPLEMENTACION DE NUESTRO VIEW HOLDER ESPECÍFICO ----------------------------
    // stores and recycles views as they are scrolled off screen
    public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        TextView tvAutor;

        MyViewHolder(View itemView) {
            super(itemView);
            tvAutor = itemView.findViewById(R.id.tvAutor);
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            if (mClickListener != null)
                mClickListener.onRVItemClick(view, getAdapterPosition());
        }
    }
    // ---------------------------------------------------------------------------------------


}

Vemos que nuestra clase MyRVAdapter extiende de RecyclerView.Adapter<MyRVAdapter.MyViewHolder> es decir, de un RecyclerView.Adapter genérico cuyo ViewHolder está definido precisamente dentro de nuestra propia clase (MyRVAdapter.MyViewHolder ) Revisemos pues los métodos que tenemos que seobrecargar.

Como hemos dicho antes el siguiente código es llamado por Android cuando va a crear un nuevo ViewHolder,

   @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = mInflater.inflate(R.layout.autor_item, parent, false);
        return new MyViewHolder(view);
    }

Le devolvemos un MyViewHolder porque llamamos al constructor de nuestra clase MyViewHolder, pasándole como parámetro la View (el layout inflado) desde el recurso layout que hemos definido.

Cuando tenemos que vincular los datos con los controles o views de nuestro layout para cada item, lo hacemos asi:

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        String autor = autores.get(position);
        holder.tvAutor.setText(autor);
    }

Android nos pasa un Holder, uno de los nuestros, y la posición que ocupa en la representación visual, por lo que vamos a nuestros datos, nuestra Lista de Strings y obtenemos el String en esa posición, luego lo colocamos en el View del holder en el que queremos, en nuestro ejemplo en el único que hay, en el TextView tvAutor.

El último que tenemos que sobrecargar es getItemCount que lo llama Android para saber cuantos items gestionamos. Fácilmente devolvemos el tamaño de nuestros datos, es decir cuantos elementos hay en nuestra lista de Strings. Que como vemos :

public class MyRVAdapter extends RecyclerView.Adapter<MyRVAdapter.MyViewHolder> {

    private List<String> autores;
    private LayoutInflater mInflater;
    private ItemClickListener mClickListener;

esta definida a nivel de clase y se carga en el constructor de nuestra clase:

    // Constructor, los datos los recibimos en el constructor.
    public MyRVAdapter(Context context, List<String> autores) {
        super();
        this.mInflater = LayoutInflater.from(context);
        this.autores = autores;
    }

Además como vemos nuestro LayoutInflater lo obtenemos desde el context que nos pasan, es decir, desde la Activity que está renderizando el RecyclerView.

Definiendo el ViewHolder específico

Veamos ahora cómo definimos el ViewHolder. Creamos una clase que hemos llamado MyViewHolder y que como vemos extiende de RecyclerView.ViewHolder y además implementa un OnClickListener generico para cualquier vista. Es por ello por lo que tenemos dentro de nuestra clase el callback genérico onClick() en el que responderemos a los eventos que Android nos pasa cuando el usuario hace click en un item, en un ViewHolder.

    // --------- IMPLEMENTACION DE NUESTRO VIEW HOLDER ESPECÍFICO ----------------------------
    // stores and recycles views as they are scrolled off screen
    public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        TextView tvAutor;

        MyViewHolder(View itemView) {
            super(itemView);
            tvAutor = itemView.findViewById(R.id.tvAutor);
            itemView.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            if (mClickListener != null)
                mClickListener.onRVItemClick(view, getAdapterPosition());
        }
    }
    // ---------------------------------------------------------------------------------------

Este ViewHolder es la instanciación en Java de nuestro layout definido como recurso para nuestro item. Como sólo tenemos un TextView en dicho layout, sólo tenemos un objeto TextView (tvAutor) en nuestra clase java, pero como he dicho, podría ser todo lo complejo que quisiéramos.

Necesitamos definir un constructor para nuestro ViewHolder, al que Android le pasará la Vista (el Layout) sobre el que ha pulsado el usuario. Lo que hacemos en el constructor es instanciar el TextView desde el recurso xml (que lo buscará en ese layout) y a su vez indicarle a Android que el listener que implementa el callback onClick() para cualquier click en ese layout es this, que en este caso es nuestra clase MyViewHolder. Por eso implementamos View.OnClickListener y por tanto sobrecargamos el método onClick(). En él, recibimos la vista en concreto sobre la que ha pulsado el usuario.

Si el mClickListener (es decir la Activity) fue registrada como listener en nuestro adaptador, no será null y por tanto podremos llamar a su método onRVItemClick (el que le hemos forzado a definir por implementar nuestra interfaz) pasándole la vista y la posición. La posición la obtenemos de un método heredado (getAdapterPosition()) que tienen los ViewHolders.

Aquí vemos cómo definimos la interfaz que tiene que implementar la Activity que quiera asignar nuestro adaptador a un RecyclerView que tenga en su layout.

    // La actividad que contenga el RecyclerViewer va a tener que implementar esta interfaz
    // para recibir los clicks en los items de nuestro recycler view.
    public interface ItemClickListener {
        void onRVItemClick(View view, int position);
    }

Ya sólo nos queda explicar de nuestro adaptador el método getItem(position) que nos va a servir para que podamos obtener el dato a partir de la posición, en este caso el nombre de un autor, en función de la posición en la lista.

    // Se llamara desde el ItemClickListener implementado en la Actividad contenedora
    public String getItem(int position) {
        return autores.get(position);
    }

Vincular el RecyclerView de la MainActivity con nuestro Adapter

Veamos directamente el código e iremos comentandolo, esto ya es más fácil …

package com.miguel.amm.basicrecyclerview;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;


public class MainActivity extends AppCompatActivity implements MyRVAdapter.ItemClickListener {

    List<String> autores;
    private RecyclerView rvAutores;
    private MyRVAdapter rvAdapter;
    private RecyclerView.LayoutManager layoutManager;
    private boolean autoresLoaded = false;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        autores = new ArrayList<String>();

        autores.add("Arquímedes");
        autores.add("Confucio");
        autores.add("Descartes");
        autores.add("Groucho Marx");
        autores.add("Julio César");
        autores.add("Nietzsche");
        autores.add("Pitágoras");
        autores.add("Platón");
        autores.add("Séneca");
        autores.add("Woody Allen");


        Button btnLoad = (Button) findViewById(R.id.btnLoad);
        rvAutores = (RecyclerView) findViewById(R.id.rvAutores);


        btnLoad.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                if (!autoresLoaded) {
                    // Al gestor de layouts será para LinearLayouts, obtenemos uno.
                    layoutManager = new LinearLayoutManager(getApplicationContext());

                    // Establecemos el gestor para nuestro recycler view.
                    rvAutores.setLayoutManager(layoutManager);

                    // Creamos un nuevo adaptador de nuestra clase
                    rvAdapter = new MyRVAdapter(getApplicationContext(), autores);

                    // Indicamos que esta activity es el listener de nuestro adapter.
                    rvAdapter.setClickListener(MainActivity.this);

                    // Establecemos el nuevo adaptador para nuestro recyler view.
                    rvAutores.setAdapter(rvAdapter);

                    autoresLoaded = true;
                }
                else{
                    Toast.makeText(getApplicationContext(), "Ya has cargado la lista de autores", Toast.LENGTH_SHORT).show();
                }

            }
        });
    }

    @Override
    public void onRVItemClick(View view, int position) {
        Toast.makeText(this, "Has pulsado en " + rvAdapter.getItem(position) + " que es el item número " + position, Toast.LENGTH_SHORT).show();
    }
}

En el onCreate, instanciamos el botón y el RecyclerView. Además creamos la lista de autores, añadiendo autores a la lista.

Todo lo que hay en el onClick() del botón podría ir fuera, en el propio onCreate() así se cargaría la lista nada más arrancar la aplicación. Pero he querido ponerlo en el onClick() para simular una decisión del usuario, en donde pudiera querer cargar la lista en un momento dado, o bien que esta lista se cargue una vez definidas alguna preferencia de la aplicación. Pero como digo si se saca fuera, al onCreate(), cargará al arrancar.

Lo que hacemos es, instanciar el LayoutManager, un LinearLayout, también podría ser un Grid u otro, en este caso un linear como si fuera un ListView. Una vez que tenemos el layoutManager se lo asignamos al RecyclerView que hemos instanciado.

Ahora vamos a asignar tambien al RecyclerView nuestro adaptador, para ello, primero lo creamos y luego le indicamos que es ésta activity la que será el listener al que propagar los eventos onClick() que Android le da a nuestro Adapter, bueno, mejor dicho al ViewHolder que implementa nuestro adapter.

En el onRVItemClick() que es el método al que propagamos los onClicks desde el adaptador, mostramos un Toast para ver que efectivamente desde la Activity capturamos el onClick en uno de los items del RecyclerView. Vemos también cómo hacemos uso de la función getItem(position) para mostrar el texto en el Toast.

El resto es la lógica necesaria para no cargar dos veces el RecylerView.

Os dejo el código completo para que podáis probarlo y ampliarlo. Además os dejo también otra versión donde se añade una imagen en cada item layout.