La vista RecyclerView

La vista RecyclerView visualiza una lista o cuadrícula deslizable de varios elementos, donde cada elemento puede definirse mediante un layout. Su utilización es algo compleja, pero muy potente. Un ejemplo lo podemos ver en la siguiente figura:
 

Dentro del API de Android encontramos las vistas ListView y GridView nos ofrece una alternativa a RecyclerView. Esta última no ha sido añadida a ningún API si no que se añade en una librería de compatibilidad. A pesar de que resulta algo más compleja de manejar, recomendamos el uso de RecyclerView, en lugar de ListView o GridView, al ser más eficiente y flexible. Aunque su uso se describe con detalle en El Gran Libro de Android Avanzado, hacemos en este punto una introducción de sus funcionalidades básicas. Las principales ventajas que ofrece RecyclerView frente a ListView o GridView son:

  • Reciclado de vistas (RecyclerView.ViewHolder)
  • Distribución de vistas configurable (LayoutManager)
  • Animaciones automáticas (ItemAnimator)
  • Separadores de elementos (ItemDecoration)
  • Trabaja conjuntamente con otros witgets introducidos en Material Design (CoordinationLayout)

video[Tutorial] Creación de listas con RecyclerView.

Crear una lista (o cuadrícula) de elementos con un RecyclerView conlleva los siguientes pasos:

  • Diseñar un Layout que contiene el RecyclerView.
  • Implementar la actividad que lo visualice el RecyclerView
  • Diseñar un Layout individual que se repetirá en la lista
  • Personalizar cada una de los Layouts individuales según nuestros datos utilizando un adaptador.
  • Definir como queremos que se posicionen los elementos en las vistas. Por ejemplo en forma de lista o de cuadricula.

Los tres primeros pasos anteriores son similares al uso de cualquier otro tipo de vista. Los dos últimos sí que requieren una explicación más extensa:

Personalizar los datos a mostrar

Para personalizar los elementos a mostrar en un RecyclerView hemos de usar un adaptador. La creación de adaptadores puede ser delicada, en algunos casos podemos tener problemas de eficiencia. Para evitar estos problemas, Google ha cambiado la forma de trabajar con RecyclerView. Ya no se puede utilizar la interfaz Adapter, si no que se ha de utilizar la clase RecyclerView.Adapter.

video[Tutorial] El patrón ViewHolder y su uso en un RecyclerView.

Distribuir los elementos

A diferencia ListView o GridView, que muestran los elementos usando una determinada configuración,  RecyclerView puede configurar esta distribución por medio de la clase LayoutManager. El sistema nos proporciona tres descendientes de LayoutManager, que son mostrados en la siguiente figura. También podemos crear nuestro descendiente de LayoutManager.

                             

En los siguientes ejercicios usaremos un RecyclerView en Mis Lugares. La actividad inicial de la aplicación nos permite escoger entre cuatro botones. En una aplicación como la desarrollada, sería mucho más interesante que en esta actividad se visualizara directamente una lista con los lugares almacenados.

Ejercicio paso a paso: Un RecyclerView en Mis Lugares

1. Añade al fichero Gradle Scripts/Bulid.gradle (Module:app) la siguiente dependencia

dependencies {
    …
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
}

NOTA: Si lo deseas, puedes saltarte este paso. Más adelante, cuando aparezca RecyclerView en el código Java, la clase aparecerá marcada en rojo, al no encontrar su declaración. Al pulsar sobre la clase, aparecerá una bombilla roja donde podrás elegir la opción Add dependency on androidx.recyclerview:recyclerview. El mismo añadirá la dependencia, con la ventaja de seleccionar la última versión disponible.

La clase ReciclerView no ha sido añadida al API de Android, si no que se encuentra en una librería externa. Esto tiene la gran ventaja de que aunque esta clase aparece en la versión 5 de Android, puede ser usada en versiones anteriores.

2. Reemplaza en el layout content_main.xml  por el  siguiente código

<androidx.recyclerview.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" />

3.  En la práctica “Recursos alternativos en Mis Lugares” se crea un recurso alternativo para este layout en res/layout-land/content_main.xml. Elimina este recurso alternativo.
NOTA: Al borrarlo has de desactiva la opción Safe delete.

4.  En la actividad MainActivity añade el código subrayado:

public class MainActivity extends AppCompatActivity {
   …
   private RecyclerView recyclerView;
   public AdaptadorLugares adaptador;

   @Override protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      …
      adaptador = ((Aplicacion) getApplication()).adaptador; 
      recyclerView = findViewById(R.id.recyclerView);
      recyclerView.setHasFixedSize(true);
      recyclerView.setLayoutManager(new LinearLayoutManager(this));
      recyclerView.setAdapter(adaptador);
   }
   … 
class MainActivity : AppCompatActivity() {
   …
  
 val lugares by lazy { (application as Aplicacion).lugares }
   val adaptador by lazy { (application as Aplicacion).adaptador }
   val usoLugar by lazy { CasosUsoLugar(this, lugares, adaptador) }
 
   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      …
      recyclerView.apply {
         setHasFixedSize(true)
         layoutManager = LinearLayoutManager(this@MainActivity)
         adapter = adaptador
      }
   }
   … 

En Java declaramos recyclerView y lo inicializamos con findViewById().  En Kotlin se hace automáticamente al estar en el Layout.  Creamos un adaptador y se lo asignamos al RecyclerView. La clase AdaptadorLugar será definida a continuación. Además, indicamos que las vistas a mostrar serán de tamaño fijo y que usaremos un LayoutManager de tipo LinearLayoutManager.
 

5.  De ser necesario, elimina del método onCreate() el código destinado a inicializar los botones. Los botones van a ser reemplazados por el RecyclerView.

6.   En la clase Aplicacion crea la variable adaptador:

public AdaptadorLugares adaptador = new AdaptadorLugares(lugares);
val adaptador = AdaptadorLugares(lugares)

7.  Ahora hemos de definir el layout que representará cada uno de los elementos de la lista. Crea el fichero res/layout/elemento_lista.xml con el siguiente código:

<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="4dp">
   <ImageView android:id="@+id/foto"
           android:layout_width="?android:attr/listPreferredItemHeight"
           android:layout_height="?android:attr/listPreferredItemHeight"
           android:contentDescription="fotografía"
           android:src="@drawable/bar"
           app:layout_constraintTop_toTopOf="parent"
           app:layout_constraintLeft_toLeftOf="parent"/>
   <TextView android:id="@+id/nombre"
           android:layout_width="0dp"
           android:layout_height="wrap_content"
           android:text="Nombres del lugar"
           android:textAppearance="?android:attr/textAppearanceMedium"
           android:textStyle="bold"
           android:maxLines="1"
           app:layout_constraintTop_toTopOf="parent"
           app:layout_constraintStart_toEndOf="@+id/foto"
           app:layout_constraintEnd_toEndOf="parent"/>
   <TextView android:id="@+id/direccion"
           android:layout_width="0dp"
           android:layout_height="wrap_content"
           android:gravity="center"
           android:maxLines="1"
           android:text="dirección del lugar"
           app:layout_constraintTop_toBottomOf="@id/nombre"
           app:layout_constraintStart_toEndOf="@+id/foto"
           app:layout_constraintEnd_toEndOf="parent"/>
   <RatingBar android:id="@+id/valoracion"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           style="?android:attr/ratingBarStyleSmall"
           android:isIndicator="true"
           android:rating="3"
           app:layout_constraintTop_toBottomOf="@id/direccion"
           app:layout_constraintLeft_toRightOf="@+id/foto"
           app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> 

Para combinar las vistas se ha escogido un ConstraintLayout. El primer elemento que contiene es un ImageView alineado a la izquierda. Su altura se establece a partir de un parámetro de configuración del sistema ?android:attr/listPreferredItemHeight (altura preferida para ítem de lista). Su altura es la misma, por lo tanto, la imagen será cuadrada. A la derecha se muestran dos textos. En el texto de mayor tamaño se visualizará el nombre del lugar y en el de menor tamaño, la dirección. Bajo estos textos se ha incluido un RatingBar.

8.  El siguiente paso será crear la clase AdaptadorLugares, que se encargará de rellenar el ReciclweView.

public class AdaptadorLugares extends 
                      RecyclerView.Adapter<AdaptadorLugares.ViewHolder> {
    protected RepositorioLugares lugares;         // Lista de lugares a mostrar
    public AdaptadorLugares(RepositorioLugares lugares) {
        this.lugares = lugares;
    }

    //Creamos nuestro ViewHolder, con los tipos de elementos a modificar
    public static class ViewHolder extends RecyclerView.ViewHolder {
       public TextView nombre, direccion;
       public ImageView foto;
       public RatingBar valoracion;
       public ViewHolder(View itemView) {
          super(itemView);
          nombre = itemView.findViewById(R.id.nombre);
          direccion = itemView.findViewById(R.id.direccion);
          foto = itemView.findViewById(R.id.foto);
          valoracion= itemView.findViewById(R.id.valoracion);
       }
       // Personalizamos un ViewHolder a partir de un lugar
       public void personaliza(Lugar lugar) {
          nombre.setText(lugar.getNombre());
          direccion.setText(lugar.getDireccion());
          int id = R.drawable.otros;
          switch(lugar.getTipo()) {
             case RESTAURANTE:id = R.drawable.restaurante; break;
             case BAR:    id = R.drawable.bar;     break;
             case COPAS:   id = R.drawable.copas;    break;
             case ESPECTACULO:id = R.drawable.espectaculos; break;
             case HOTEL:   id = R.drawable.hotel;    break;
             case COMPRAS:  id = R.drawable.compras;   break;
             case EDUCACION: id = R.drawable.educacion;  break;
             case DEPORTE:  id = R.drawable.deporte;   break;
             case NATURALEZA: id = R.drawable.naturaleza; break;
             case GASOLINERA: id = R.drawable.gasolinera; break;  }
          foto.setImageResource(id);
          foto.setScaleType(ImageView.ScaleType.FIT_END);
          valoracion.setRating(lugar.getValoracion());
       }
    }

    // Creamos el ViewHolder con la vista de un elemento sin personalizar
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflamos la vista desde el xml
        View v = LayoutInflater.from(parent.getContext())
                         .inflate(R.layout.elemento_lista, parent, false);
        return new ViewHolder(v);
    }

    // Usando como base el ViewHolder y lo personalizamos
    @Override
    public void onBindViewHolder(ViewHolder holder, int posicion) {
        Lugar lugar = lugares.elemento(posicion);
        holder.personaliza(lugar);
    }
    // Indicamos el número de elementos de la lista
    @Override public int getItemCount() {
        return lugares.tamanyo();
    }
} 
class AdaptadorLugares(private val lugares: RepositorioLugares) :
                     RecyclerView.Adapter<AdaptadorLugares.ViewHolder>() {

   class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {

      fun personaliza(lugar: Lugar) = with(itemView){
         nombre.text = lugar.nombre
         direccion.text = lugar.direccion
         foto.setImageResource(when (lugar.tipoLugar) {
            TipoLugar.RESTAURANTE -> R.drawable.restaurante
            TipoLugar.BAR -> R.drawable.bar
            TipoLugar.COPAS -> R.drawable.copas
            TipoLugar.ESPECTACULO -> R.drawable.espectaculos
            TipoLugar.HOTEL -> R.drawable.hotel
            TipoLugar.COMPRAS -> R.drawable.compras
            TipoLugar.EDUCACION -> R.drawable.educacion
            TipoLugar.DEPORTE -> R.drawable.deporte
            TipoLugar.NATURALEZA -> R.drawable.naturaleza
            TipoLugar.GASOLINERA -> R.drawable.gasolinera
            TipoLugar.OTROS -> R.drawable.otros
         })
         foto.setScaleType(ImageView.ScaleType.FIT_END)
         valoracion.rating = lugar.valoracion
      }
   }

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
                                                             ViewHolder {
      val v = LayoutInflater.from(parent.context)
                          .inflate(R.layout.elemento_lista, parent, false)
      return ViewHolder(v)
   }

   override fun onBindViewHolder(holder: ViewHolder, posicion: Int) {
      val lugar = lugares.elemento(posicion)
      holder.personaliza(lugar)
   }

   override fun getItemCount() = lugares.tamanyo()
} 

Un adaptador es un mecanismo estándar en Android que nos permite crear una serie de vistas que han de ser mostradas dentro de un contenedor. Con las vistas ListView, GridView, Spiner o Gallery has de crear el adaptador utilizando la interfaz Adapter.  Pero con RecyclerView  has de utilizar la clase RecyclerView.Adapter.

En el constructor se inicializa el conjunto de datos a mostrar (en el ejemplo lugares) y otras variables globales a la clase. El objeto inflador nos va a permitir crear una vista a partir de su XML.

Luego se crea la clase ViewHolder, que contendrá las vistas que queremos modificar de un elemento (en concreto: dos TextView con el nombre y la dirección, un ImageView con la imagen del tipo de lugar y un RatingBar). Esta clase es utilizada para evitar tener que crear las vistas de cada elemento desde cero. Lo va a hacer es utilizar un ViewHolder que contendrá las cuatro vistas ya creadas, pero sin personalizar. De forma que, gastará el mismo ViewHolder para todos los elementos y simplemente lo personalizaremos según la posición. Es decir, reciclamos el ViewHolder. Esta forma de proceder mejora el rendimiento del ReciclerView, haciendo que funcione más rápido.

El método onCreateViewHolder() devuelve una vista de un elemento sin personalizar. Podríamos definir diferentes vistas para diferentes tipos de elementos utilizando el parámetro viewType.Usamos el método inflate() para crear una vista a partir del layout XML definido en elemento_lista. En este método se indica como segundo parámetro el layout padre que contendrá a la vista que se va a crear. En este caso, resulta imprescindible indicarlo, ya que queremos que la vista hijo ha de adaptarse al tamaño del padre (en elemento_lista se ha indicado layout_width="match_parent").  El tercer parámetro del método permite indicar si queremos que la vista sea insertada en el padre. Indicamos false, dado que esta operación la va a hacer el ReciclerView.

El método onBindViewHolder() personaliza un elemento de tipo ViewHolder según su posicion. A partir del ViewHolder que personalizamos ya es el sistema quien se encarga de crear la vista definitiva que será insertada en el ReciclerView. Finalmente, el método getItemCount() se utiliza para indicar el número de elementos a visualizar.

9.       Ejecuta la aplicación y verifica el resultado.

Ejercicio paso a paso: Selección de un elemento en un RecyclerView

En este ejercicio veremos cómo detectar que se ha pulsado sobre uno de los elementos del RecyclerView. En las vistas ListView y GridView podíamos realizar esta tarea usando el método setOnItemClickListener(). Sin embargo, en RecyclerView no se ha incluido este método. Google prefiere que asignemos un escuchador de forma independiente a cada una de las vistas que va a contener RecyclerView. Existen muchas alternativas para hacer este trabajo (Escuchadores de eventos y manejadores de eventos en Android). A continuación, explicamos una de ellas:

1.    En Java añade a la clase AdaptadorLugares la siguiente declaración:

protected View.OnClickListener onClickListener;

      Para poder modificar el campo anterior añade el siguiente setter:

public void setOnItemClickListener(View.OnClickListener onClickListener) {
    this.onClickListener = onClickListener;
}

      En Kotlin añade una nueva propiedad :

class AdaptadorLugares(val lugares: RepositorioLugares) : …
 
   lateinit val onClick: (View) -> Unit

2.    Solo nos queda aplicar este escuchador a cada una de las vistas creadas. Añade la línea subrayada en el método indicado:

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    // Inflamos la vista desde el xml
    View v = inflador.inflate(R.layout.elemento_lista, null);
    v.setOnClickListener(onClickListener);
    return new ViewHolder(v);
}
fun personaliza(lugar: Lugar, onClick: (View) -> Unit) = with(itemView) {
   …
   setOnClickListener{ onClick(itemView) }
}

3.    Desde la clase MainActivity vamos a asignar un escuchador. Para ello añade el siguiente código al final del método onCreate():

adaptador.setOnItemClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        int pos = recyclerView.getChildAdapterPosition(v);
        usoLugar.mostrar(pos);
    }
});
adaptador.onClick  =  {
   val pos = recyclerView.getChildAdapterPosition(it)
   usoLugar.mostrar(pos)
}

El método getChildAdapterPosition(), nos indicarán la posición de una vista dentro del adaptador.

4.    Ejecuta la aplicación y verifica el resultado.

Preguntas de repaso: RecyclerView