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.