Seguro que has visto muchos ejemplos donde un RecyclerView
recibe un listener para, por ejemplo realizar, una acción cuando se hace click en el elemento.
class MoviesAdapter(private val listener: (Movie) -> Unit) : ListAdapter<Movie, MoviesAdapter.ViewHolder>(...) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { ... } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val movie = getItem(position) holder.bind(movie) holder.itemView.setOnClickListener { listener(movie) } } }
Pero imagina que ahora queremos tener una acción extra, que nos permita marcar un elemento como favorito. ¿Qué hacemos?
El otro día justo recibía esta pregunta en la comunidad de Discord:

Te voy a mostrar tres opciones, y la tercera seguro que te va a sorprender 🤯
Opción 1: Pasar un nuevo listener
La opción más evidente puede ser añadir otra lambda al constructor, que nos permita gestionar esa acción de forma independiente:
class MoviesAdapter( private val onClick: (Movie) -> Unit, private val onFavorite: (Movie) -> Unit )
Esto está muy bien, ahora le podemos pasar este listener al ViewHolder, que se llame cuando se haga click en la acción, y desde fuera hacer lo que necesitemos:
private val adapter = MoviesAdapter( onClick = { ... }, onFavorite = { viewModel.onFavorite(it)} )
Pero te puedes estar preguntando… ¿qué pasa si aparece una nueva acción, como por ejemplo “Compartir”? ¿Y qué pasa si hay 10 más? ¿Le añado 12 listeners al constructor?
Obviamente esta solución no escala bien, y solo te la recomendaría si tienes la seguridad de que no van a crecer el número de acciones
Opción 2: Modelar las acciones con una sealed class
En vez de crear listeners para cada acción, ¿por qué no generalizamos la idea de que un RecyclerView
puede tener un listado indeterminado de acciones?
De esta manera, la forma de la lambda sería:
onAction: (Action) -> Unit
Hay muchas formas de modelar esto. De hecho, podríamos pasarle 2 valores: la acción (que podría ser un enum
), y la película sobre la que se realiza la acción.
Pero para no limitarnos, porque podría ser que una acción requiriera 0 o más de un valor, lo vamos a modelar como una sealed class
. Así nos curamos en salud para el futuro.
Si queremos obligar a que al menos reciban el elemento sobre el que se ha realizado la acción, podemos usar efectivamente una sealed class
:
sealed class Action(val movie: Movie) { class Click(movie: Movie) : Action(movie) class Favorite(movie: Movie) : Action(movie) class Share(movie: Movie) : Action(movie) class Delete(movie: Movie) : Action(movie) }
Si por el contrario, queremos dejar mayor flexibilidad, puede ser suficiente con una sealed interface
:
sealed interface Action { class Click(val movie: Movie) : Action class Favorite(val movie: Movie) : Action class Share(val movie: Movie) : Action class Delete(val movie: Movie) : Action }
En cualquier caso, la forma de usarla luego será similar. El ViewHolder
recibirá la función de callback, y la llamará cuando sea necesario:
class ViewHolder(view: View, private val onAction: (Action) -> Unit) : RecyclerView.ViewHolder(view) { private val binding = ViewMovieBinding.bind(view) fun bind(movie: Movie) { binding.movie = movie binding.favorite.setOnClickListener { onAction(Action.Favorite(movie)) } binding.share.setOnClickListener { onAction(Action.Share(movie)) } } }
Finalmente, la implementación de la función es bastante directa. Si estamos usando un ViewModel
, podemos tener una función que directamente reciba la acción:
private val adapter = MoviesAdapter { viewModel.onAction(it) }
Y luego la implementación del ViewModel
sería:
fun onAction(action: Action) { when(action){ is Action.Click -> TODO() is Action.Delete -> TODO() is Action.Favorite -> TODO() is Action.Share -> TODO() } }
Aquí ya implementarías como necesitaras cada acción, probablemente llamando a un Repository
o un UseCase
.
Opción 3: Acciones en el UiState
Esta es una alternativa que, la verdad, no me la había planteado hasta que no la vi en las nuevas guías de arquitecturas Android.
Consiste en convertir el modelo en un estado de UI que, además, sepa cómo gestionar las acciones sobre el mismo.
Para ello, nos creamos un objeto MoviesUiState
que contenga exclusivamente lo que la UI necesita:
data class MovieUiState( val id: Int, val title: String, val posterPath: String, val favorite: Boolean, val onFavorite: () -> Unit, val onDelete: () -> Unit, val onShare: () -> Unit )
El onClick()
en este caso lo dejamos fuera, porque pertenece a la lógica de UI (al final lo que hará es navegar), y no nos interesa que pase por el ViewModel
.
Después, convertimos los elementos que nos vienen a este nuevo estado:
movies = movies.map { it.toUiState() }) private fun Movie.toUiState() = MovieUiState( id = id, title = title, posterPath = posterPath, favorite = favorite, onFavorite = { viewModelScope.launch { switchMovieFavoriteUseCase(this@toUiState) } }, onShare = { TODO() }, onDelete = { TODO() } )
Como este estado se crea en el propio ViewModel
(o en el lugar donde hayas decidido tener la lógica de negocio), ya puedes llamar directamente a quien se encargue de ejecutar esa acción.
En mi caso, como ejemplo, llamo al caso de uso que gestiona los favoritos (es una función suspend
, por eso necesita estar en un contexto de corrutina).
Te dejo los otros dos como ejercicio si quieres realizarlos.
Lo último que quedaría es modificar el adapter, para que pueda usar este nuevo estado. Te dejo el ViewHolder
a modo de ejemplo:
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val binding = ViewMovieBinding.bind(view) fun bind(movie: MainViewModel.MovieUiState) { binding.movie = movie binding.favorite.setOnClickListener { movie.onFavorite() } binding.share.setOnClickListener { movie.onShare() } binding.delete.setOnClickListener { movie.onDelete() } } }
La verdad que esta solución, aunque en principio puede resultar más limpia, porque no tenemos que pasar por la Activity
o el Fragment
, también creo que presenta un par de inconvenientes.
- El código resultante es más verboso, y aunque hacer una transformación del modelo que no siempre es necesaria
- Nos limita un poco, porque si algunas de las acciones son lógica de UI, vamos a tener que mantener también una solución como los puntos 1 ó 2. Por ejemplo, la acción de compartir muchas veces será simplemente código de UI, y no es necesario pasar por el
ViewModel
.
Si me preguntas con cuál me quedaría, mi solución sería la 1 cuando solo hay 1 ó 2 listeners, y la 2 cuando ya hay más.
¿Y a ti qué te parecen estas soluciones? ¿Hay alguna que no conocieras de antes? ¿Conoces alguna otra que no haya mencionado?
Te leo en los comentarios.
0 comentarios