Tiempo de lectura: 11 minutos
En este post vamos a ver cómo hacer un adaptador para un RecyclerView cumpliendo, al menos, dos regla fundamentales de los principios SOLID: Single Responsability y Open/Close, es decir, que tenga sólo una responsabilidad y este abierto a extensión pero cerrado a modificación.
 
Todo el código está disponible en mi GitHub y lo puedes usar como adaptador en todos tus proyectos.
En este post veremos:
  • Implementación de un Adapter para un RecyclerView que cumpla los principios SOLID.
  • Implementación y estructura de un Adapter genérico para todos los proyectos.
  • Patrón Visitor.
  • ViewModels y su uso.

La sdk de Android nos ofrece una clase RecyclerView.Adapter que tiene como objetivo ‘adaptar‘ nuestros datos al componente visual que nos ofrece la propia sdk: RecyclerView. Está desarrolla con un patrón Adapter.

Esta clase tiene unos método abstractos que tenemos que implementar y que sirven de ayuda para que el componente sepa visualizarlos. 

Hay que tener muy claro que la única responsabilidad del RecyclerView.Adapter es transformar nuestros modelos de datos a objetos que entienda el componente. Esto aunque parece obvio puede llegar a no serlo pues es muy común modelar nuestras vistas en el propio adaptador. 

Para empezar, veamos un mal ejemplo de cómo diferenciar los tipos de vista que va a tener el listado.

Analicemos esta clase por partes.

Empecemos con los tipos de vista que se quieren mostrar en el listado:

Como podemos ver en este código, si necesitáramos meter un nuevo tipo de vista, habría que cambiar este método para añadir los nuevos tipos de vista que se fueran incluyendo, por lo que estamos acoplando nuestras vista al adaptador.

Esto mismo pasaría en el método del adaptador que nos permite crearnos el ViewHolder (donde inflamos la vista):

Al igual que en el ejemplo anterior, un cambio en un tipo de vista provocaría un cambio aquí.

Por último, algunos desarrolladores suelen “rellenar” su vista de datos de la siguiente forma:

Aquí ya no sólo habría que diferenciar entre los distintos tipos de vistas, sino que habría que establecer los datos por cada tipo de ViewHolder que se tenga.

Cómo hemos podido ver en este BadAdapter, si hacemos esto en nuestro código ya estamos incumpliendo los principios SOLID que hemos mencionado anteriormente:

  • La clase ya hace más de una cosa: adapta nuestros datos y establece los datos de cada uno de los registros.
  • Si tenemos que añadir un nuevo tipo de vista, tenemos que modificar esta clase.
  • Si tenemos que añadir un elemento más en nuestra vista, tenemos que modificar esta clase.

Con este código hemos acoplado el adaptador a nuestra vista. Cualquier cambio que se produzca en la vista, afectará también al adaptador. Para evitar esto, tenemos que dejar que el adaptador sólo cumpla su responsabilidad, por lo que fue creado y ofrecido: adaptar datos. Veamos cómo podríamos hacerlo:

Ahora el adaptador hace una única cosa:adaptar datos. Vayamos por partes:

  • Tipos de Vista: getItemVIewType. En este método hay que indicarle a través de un entero los distintos tipos de vista que tenemos, para ello usaremos una implementación del patrón Visitor que nos va a permitir pasarle como tipo el ID que se obtiene del propio layout(vista), ej: R.layout.view_player.
  • Crear ViewHolder: onCreateViewHolder. La forma que tenemos de inflar nuestro layout es a través del typeView que recibimos y que en el anterior método se lo pasamos al adaptador como ID del layout (R.layout.view_player). Por cada tipo de vista a visualizar, tendrá su propio layout. En nuestra estructura definimos una clase padre de todos los ViewHolder y es esta la que le devolvemos.
  • Cargas datos de nuestro modelo a la vista: onBindViewHolder. Aquí recibimos el ViewHolder creado en el anterior método. Lo único que hacemos es pasarle el modelo al ViewHolder para que se encargue el de establecer sus propios datos.

Una vez visto cómo crear un adaptador desacoplado de nuestras vistas, ¿que pasaría si tuviéramos que cambiar nuestro modelo de vista? o, ¿qué pasaría si tuviéramos que añadir un nuevo tipo de vista?. Aquí es donde el patrón Visitor nos puede echar una mano.

Lo que nos va a permitir el patrón Visitor, es deshacernos de las condiciones anidadas para comprobar el tipo de vista y delegar esto en otra clase.

Patrón Visitor para implementar el Adaptador.

Se crea una interfaz donde se define cada uno de los ViewModels que va a aceptar esta estructura. En nuestro caso, tenemos 4 tipos de ViewModels que queremos representar en el RecyclerView. Además, también definimos un método ‘holder’ que nos permitirá crearnos el ViewHolder según el tipo que se reciba. 

Con este patrón evitamos tener que hacer los if .. else o los instanceof.

Aunque vamos a tener cuatro tipos de ViewModels necesitamos trabajar con un sólo tipo de modelo de datos y que a través de un método le diga el tipo. Para ello, nos creamos una interfaz que va a ser implementada por todos los ViewModels que quieran visualizarse en el RecyclerView.

Todos los ViewModels tendrás que implementar un método que recibirán como parámetro la interfaz creada anteriormente y que nos va a permitir ahorrarnos el if..else.

Esta sería la implementación de la interfaz anterior.

Esta clase es un ViewModel llamada PlayerViewModel. Prestemos atención a la implementación del método de la interfaz ItemVisitable. Recibimos la interfaz donde se indicaba cada uno de los ViewModels que vamos a tener y como tipo le pasamos una referencia de la propia clase (Ver TypeViewModelFactory más arriba).

Prestemos atención a la implementación de esta clase:

FootballTypeViewModel tiene la responsabilidad de ofrecer la información sobre las vistas al adaptador. Quizás el holder se podría sacar a otra clase, pero como guarda relación con los tipos de vista, me parece buena idea dejarlo todo junto.

Aquí lo que hacemos es que cuando se reciba un tipo de ViewModel (esto es gracias al método que se implementa en los ViewModels) le pasamos el ID del Layout donde queremos pintarlo y que está declarado en el ViewHolder. Por último se devuelve una instancia del ViewHolder según el tipo.

Por último, tenemos las clases ViewHolder que heredan de BaseViewHolder y se creará una por cada tipo de representación que se quiera hacer en el RecyclerView. Es aquí donde establecemos los datos del modelo. Es aquí donde pasamos de un modelo de datos a una representación gráfica. Esta es la misión del ViewHolder.

Y aquí la forma de usarlo:

 

Conclusiones

Como resumen del código expuesto, podemos decir:

  • El adaptador sólo tiene una responsabilidad: convertir nuestro listado al modelo que necesita el componente.
  • Si necesitamos añadir un nuevo tipo de vista, nuestro adapter no cambia.
  • Si necesitamos modificar una vista (ViewHolder) nuestro adapter no cambia.
  • Cualquier modificación de tipos de vista, lo hacemos en las clases generadas por el patrón Visitor. Si necesitamos modificar una vista, iremos a la propia vista a modificarla, al ViewHolder. Si quisiéramos añadir un nuevo tipo de vista, bastaría con crearnos un ViewHolder y añadirla como ‘visitable’ en el patrón.
  • Ahora ya cumple los dos objetivos propuestos:
    • La clase adaptador sólo hace una única cosa.
    • La clase está cerrada a modificación y abierta a extensión.

Esta estructura vale para cualquier tipo de proyecto. Se crean más clases pero haremos un código flexible a posibles cambios. Creo que el esfuerzo de crearlas merece la pena.

Referencias