Cómo crear vistas distintas en un Adapter de RecyclerView según el tipo de datos
Antonio Leiva

Antes de empezar, es importante mencionar que el RecyclerView es una vista contenedora que permite mostrar una lista de elementos en forma eficiente. Al utilizar un adapter, podemos controlar cómo se deben mostrar cada uno de esos elementos en la lista.

Imagina que estamos trabajando con los siguientes tipos de datos. Un conjunto de tipos multimedia, de los cuales tenemos por ejemplo los siguientes:

sealed class MediaItem(val title: String) {
    class Movie(title: String) : MediaItem(title)
    class TvShow(title: String) : MediaItem(title)
    class Album(title: String) : MediaItem(title)
}

Para crear un adapter que muestre distintas vistas en función del tipo de datos, deberemos seguir los siguientes pasos:

Crea el Adapter

Definir una clase que herede de RecyclerView.Adapter y que tenga una clase interna ViewHolder.

Esta será abstracta, y solo tendrá una función estática para crear un ViewHolder distinto en función del tipo de datos.

Cada ViewHolder específico sabrá cómo hacer bind de sus vistas específicas:

abstract class MediaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    companion object {
        fun create(parent: ViewGroup, viewType: Int): MediaViewHolder {
            val type = Type.values()[viewType]
            return when (type) {
                Type.MOVIE -> MovieViewHolder(parent.inflate(R.layout.item_movie))
                Type.TV_SHOW -> TvShowViewHolder(parent.inflate(R.layout.item_tv_show))
                Type.ALBUM -> AlbumViewHolder(parent.inflate(R.layout.item_album))
            }
        }
    }
}

class MovieViewHolder(itemView: View) : MediaViewHolder(itemView) {
    val binding = ItemMovieBinding.bind(itemView)

    fun bind(item: MediaItem.Movie) {
        // TODO
    }
}

class TvShowViewHolder(itemView: View) : MediaViewHolder(itemView) {
    val binding = ItemTvShowBinding.bind(itemView)

    fun bind(item: MediaItem.TvShow) {
        // TODO
    }
}

class AlbumViewHolder(itemView: View) : MediaViewHolder(itemView) {
    val binding = ItemAlbumBinding.bind(itemView)

    fun bind(item: MediaItem.Album) {
        // TODO
    }
}

Implementar los métodos necesarios del Adapter

En la clase Adapter, implementar los métodos onCreateViewHolder y onBindViewHolder.

El método onCreateViewHolder se encargará de crear una nueva vista y un nuevo ViewHolder cada vez que sea necesario, mientras que el método onBindViewHolder se encargará de actualizar los datos de la vista con la información del elemento correspondiente.

Además, necesitas el método getItemViewType, que indicará según la posición, cuál es el tipo de datos.

Para hacerlo más sencillo, vamos a crear un enum class que indique los tipos de datos:

enum class Type {
    MOVIE, TV_SHOW, ALBUM
}

Y ahora ya puedes definir las funciones:

class MediaAdapter : ListAdapter<MediaItem, MediaViewHolder>(DiffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder =
        MediaViewHolder.create(parent, viewType)

    override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
        when (val item = getItem(position)) {
            is MediaItem.Movie -> (holder as MovieViewHolder).bind(item)
            is MediaItem.TvShow -> (holder as TvShowViewHolder).bind(item)
            is MediaItem.Album -> (holder as AlbumViewHolder).bind(item)
        }
    }

    override fun getItemViewType(position: Int): Int = when (getItem(position)) {
        is MediaItem.Movie -> Type.MOVIE
        is MediaItem.TvShow -> Type.TV_SHOW
        is MediaItem.Album -> Type.ALBUM
    }.ordinal
}

Trabajando con el concepto de Renderers

La solución anterior no es incorrecta, pero si el número de tipos es muy grande, puede ser muy difícil de escalar.

Una alternativa es crear un Adapter que entienda el concepto de Renderer. Un Renderer no es más que una clase que sabe cómo crear una vista y cómo hacerle el bind:

interface Renderer {
    fun createView(parent: ViewGroup): View
    fun bindView(view: View, item: Any)
}

Podemos crear un Renderer por tipo de datos:

class MovieRenderer : Renderer {
    override fun createView(parent: ViewGroup): View =
        parent.inflate(R.layout.item_movie)

    override fun bindView(view: View, item: Any) {
        // TODO
    }
}

class TvShowRenderer : Renderer {
    override fun createView(parent: ViewGroup): View =
        parent.inflate(R.layout.item_tv_show)

    override fun bindView(view: View, item: Any) {
        // TODO
    }
}

class AlbumRenderer : Renderer {
    override fun createView(parent: ViewGroup): View =
        parent.inflate(R.layout.item_album)

    override fun bindView(view: View, item: Any) {
        // TODO
    }
}

Y ya solo nos queda asignar el Renderer correspondiente a cada tipo de datos:

private val renderers = mapOf(
    MediaItem.Movie::class to MovieRenderer(),
    MediaItem.Movie::class to TvShowRenderer(),
    MediaItem.Movie::class to AlbumRenderer()
)

¿Cómo sería un Adapter que entienda estos renderers? Un código similar a este:

class RendererAdapter(private val renderers: Map<KClass<*>, Renderer>) :
    ListAdapter<MediaItem, RendererViewHolder>(DiffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RendererViewHolder {
        val renderer = renderers.values.toList()[viewType]
        return RendererViewHolder(parent, renderer)
    }

    override fun onBindViewHolder(holder: RendererViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    override fun getItemViewType(position: Int): Int {
        renderers.entries.forEachIndexed { index, entry ->
            if (entry.key == getItem(position)::class) return index
        }
        throw IllegalArgumentException("Unknown type")
    }
}

Ahora, simplemente pasándole distintos renderers al adapter, podemos crear vistas totalmente distintas con un único adapter.

Quizá también te interese…

Cómo simular una base de datos reactiva en Room con Fakes

Cómo simular una base de datos reactiva en Room con Fakes

En el desarrollo de aplicaciones móviles es muy común utilizar bases de datos para almacenar y gestionar la información que se utiliza en la aplicación. En el caso de Android, una de las opciones más populares es Room, una librería de persistencia de datos que...

Descargar una página web en Android con OkHttp

Descargar una página web en Android con OkHttp

En este tutorial vamos a aprender cómo descargar una página web en Android utilizando la librería OkHttp y la librería activity-ktx para facilitar el manejo de los ciclos de vida de nuestra aplicación. Configuración de la App Para empezar, necesitamos incluir las...

Usar Ktor Client para hacer peticiones HTTP en Android

Usar Ktor Client para hacer peticiones HTTP en Android

Ktor es un framework de servidor y cliente de Kotlin diseñado para crear aplicaciones web y móviles de forma rápida y fácil. En este artículo, veremos cómo usar Ktor client en una aplicación Android para hacer peticiones a una API. Configurar las dependencias de Ktor...

0 comentarios

Enviar un comentario

Los datos personales que proporciones a través de este formulario quedarán registrados en un fichero de DevExpert, S.L.U., con el fin de gestionar los comentarios que realizas en este blog. La legitimación se realiza a través del consentimiento de la parte interesada. Si no se acepta, no podrás comentar en este blog. Los datos que proporciona solo se utilizan para evitar el correo no deseado y no se usarán para nada más. Puede ejercer los derechos de acceso, rectificación, cancelación y oposición en contacto@devexperto.com.

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Acepto la política de privacidad *