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. Sin embargo, sería mucho más interesante que en esta actividad se visualizara directamente una lista con los lugares.

Ejercicio: 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.2.1'
}

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. Realmente, solo puede ser utilizada a partir del nivel de API 7. Pero el 100 % de los dispositivos que se comercializan en la actualidad cumplen este requisito.

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 desactivar la opción Safe delete.
Elimina también el resto de recursos alternativos para content_main.xml.

4.  El layout content_main se encuentra incrustado dentro activity_main por medio de un include. Para poder acceder a las vistas del layout incrustado usando View Binding, es necesario que el include tenga un id. Añade el código subrayado en el layout activity_main:
 

android:id="@+id/content" layout="@layout/content_main" />
5.  En la actividad MainActivity añade el código subrayado:

public class MainActivity extends AppCompatActivity {
   …
   public AdaptadorLugares adaptador;

   @Override 
   protected void onCreate(Bundle savedInstanceState) {
   	 
     …
      adaptador = ((Aplicacion) getApplication()).adaptador; 
      RecyclerView recyclerView = binding.content.recyclerView;
      recyclerView.setHasFixedSize(true);
      recyclerView.setLayoutManager(new LinearLayoutManager(this));
      recyclerView.setAdapter(adaptador);
   }
   … 
class MainActivity : AppCompatActivity() {
   …
   lateinit var adaptador: AdaptadorLugares
 
   override fun onCreate(savedInstanceState: Bundle?) {
      …
      usoLugar = CasosUsoLugar(this, lugares, adaptador)
      adaptador = (application as Aplicacion).adaptador
      binding.content.recyclerView.apply {
         setHasFixedSize(true)
         layoutManager = LinearLayoutManager(this@MainActivity)
         adapter = adaptador
      }
   }
   … 

Declaramos recyclerView y lo inicializamos con binding.content.recyclerView. Recuerda que content es el id del layout incrustado. Declaramos un adaptador y lo inicializamos desde la clase Aplicación.. 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. Finalmente, asignamos el adaptador al RecyclerView.

6.  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.

7.   En la clase Aplicacion crea la variable adaptador:

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

8.  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.

9.  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(ElementoListaBinding itemView) {
          super(itemView.getRoot());
          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
        ElementoListaBinding v = ElementoListaBinding.inflate(LayoutInflater
                               .from(parent.getContext()), parent, false);
      return new AdaptadorLugares.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.tamaño();
    }
} 
class AdaptadorLugares(private val lugares: RepositorioLugares) :
                     RecyclerView.Adapter<AdaptadorLugares.ViewHolder>() {

   class ViewHolder(val view: ElementoListaBinding) : 
                                     RecyclerView.ViewHolder(view.root) {

      fun personaliza(lugar: Lugar) = with(View){
         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 binding = ElementoListaBinding.inflate(
                      LayoutInflater.from(parent.context),  parent, false)
      return ViewHolder(binding)
   }

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

   override fun getItemCount() = lugares.tamaño()
} 

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.

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

Ejercicio: Selección de un elemento en un RecyclerView

En Queremos que cuando se seleccione un elemento de la actividad principal de Mis Lugares, se abra la actividad que visualiza su contenido. Va a ser similar al ejercicio Selección de un elemento en un RecyclerView que vimos para Asteroides. La mayor diferencia es que ahora usaremos View Binding.
 

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

protected View.OnClickListener onClickListener;
   	  
lateinit var onClick: (View) -> Unit  

2.Para poder modificar el campo anterior añade el siguiente setter, solo en Java:

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

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

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ElementoListaBinding v = ElementoListaBinding.inflate(LayoutInflater
                               .from(parent.getContext()), parent, false);
    v.getRoot().setOnClickListener(onClickListener);
    return new AdaptadorLugares.ViewHolder(v);
}
fun personaliza(lugar: Lugar, onClick: (View) -> Unit) = with(itemView) {
   …
   setOnClickListener{ onClick(itemView) }
}
…
override fun onBindViewHolder(holder: ViewHolder, posicion: Int) {
    …
    holder.personaliza(lugar, onClick)
}

4.    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= binding.content.recyclerView.getChildAdapterPosition(v);
        usoLugar.mostrar(pos);
    }
});
adaptador.onClick = {
   val pos: Int = binding.content.recyclerView.getChildAdapterPosition(it)
   usoLugar.mostrar(pos)
}

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

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

ejerEjercicio: Refrescar el RecyclerView de Mis Lugares

Si borras o editas un lugar y regrsas al RecyclerView donde se muestran los lugares, comprovarás que en la lista no se han actualizado. Para resolverlo sigue los siguientes pasos:
1.  Añade en MainActivity el siguiente método:

@Override protected void onResume(){
   super.onResume();
   adaptador.notifyDataSetChanged();
}
override fun onResume() {
   super.onResume()
   adaptador.notifyDataSetChanged()
}

El método onResume() es ejecutado cada vez que nuestra actividad vuelve a pasar a primer plano. Pertenece a una de las funciones del ciclo de vida de una actividad, que será estudiado más adelante. En este método, tras llamar al super, le indicamos al adaptador que los datos pueden haber cambiado y que vuelva a generar cada una de las vistas que componen la lista. 
2.  Ejecuta la aplicación y verifica el resultado.

Preguntas de repaso: RecyclerView