En el artículo anterior veíamos un ejemplo sobre Flow, pero eran los típicos ejemplos que en el día a día no nos llevan a ninguna parte.
¿Cómo encaja todo esto dentro de una aplicación Android?
Hoy vamos a ver un ejemplo
La App
Vamos a usar de base una aplicación relativamente sencilla, un listado de películas que se recuperan de un servidor y se almacenan en una base de datos.
Esto usando una arquitectura también muy sencilla con un patrón de presentación MVVM y un repository que se encarga de los datos. Si quieres saber más, echa un vistazo a mi artículo sobre Clean Architecture, aunque en este caso me he saltado alguna capa para simplificar el ejemplo.
El repository tiene dos fuentes de datos: el servidor, y la base de datos. Si encuentra datos en la base de datos, devolverá estos, y si no se irá al servidor para buscarlos.
suspend fun getMovies(): List<Movie> { if (localDataSource.isEmpty()) { val movies =remoteDataSource.getMovies() localDataSource.saveMovies(movies) } return localDataSource.getMovies() }
Todo el código está montado con corrutinas, de ahí que el repositorio sea una función suspend
. También se está usando la compatibilidad con corrutinas de Room y de Retrofit.
Creando un Flow
Tal y como está montado el proyecto, no tiene mucho sentido usar un Flow, porque solo vamos a recibir datos una única vez del repositorio.
Flow merece la pena usarlo cuando la fuente de datos está “viva”, en el sentido de que va variando en el tiempo y nosotros queremos escuchar esos cambios.
Así que vamos a hacer un pequeño hack. Vamos a simular con un aleatorio que los valores cambian cada cierto tiempo.
Empezamos creando el Flow. Para devolver un Flow no hace falta usar una función suspend
, ya que el Flow no se ejecuta hasta que lo recolectemos, y ahí es donde sí que necesitamos una corrutina.
fun getMovies(): Flow<List<Movie>> = flow { ... }
Ahora tenemos que decirle qué valores emitir. Volvemos a poner el código anterior:
fun getMovies(): Flow<List<Movie>> = flow { if (localDataSource.isEmpty()) { val movies = remoteDataSource.getMovies() localDataSource.saveMovies(movies) } ... }
Pero en vez del de devolver el resultado, lo emitimos:
fun getMovies(): Flow<List<Movie>> = flow { if (localDataSource.isEmpty()) { val movies = remoteDataSource.getMovies() localDataSource.saveMovies(movies) } emit(localDataSource.getMovies()) }
Ya estaría transformado. Lo único que este Flow solo emite un valor, así que no nos aporta demasiado. Para darle un poco de vidilla, haríamos:
while (true) { delay(2000) emit(localDataSource.getMovies().shuffled()) }
Y así emitimos valores de forma infinita. No nos tenemos por qué preocupar porque el Flow acabe. Cuando el recolector se desconecte o el scope de la corrutina desaparezca, esta corrutina también se cancelará, ya que delay
comprueba si la corrutina está finalizada antes de ejecutarse.
Cómo recolectar los datos del Flow
Ahora tienes dos maneras de hacer esto. La más directa es usar el collect
que ya vimos en el artículo anterior:
viewModelScope.launch { repository.getMovies().collect { list -> _movies.value = list } }
Pero con Android-KTX, tenemos una función que nos permite convertir un Flow en un LiveData. En el build.gradle
añade la siguiente dependencia:
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
Y a partir de aquí podrás utilizar la función asLiveData()
sobre cualquier Flow:
val movies: LiveData<List<Movie>> get() = repository.getMovies().asLiveData()
Ejecuta el proyecto, y mira cómo la App baila 💃
Conclusión
Si te fijas, usar Flow es muy sencillo una vez que tienes todos los conceptos de corrutinas asimilados. El API es tan simple que en unos minutos puedes estar creando tus propios Flows y comunicándote de forma asíncrona con la interfaz de usuario.
Además con las utilidades que incluyen las librerías de Android, trabajar con Flow cada día es más y más fácil.
Pero esto no se queda aquí. En el próximo artículo vamos a ver cómo integrar Room con Flow y haremos un ejemplo sencillo de paginación, para que veas un caso aún más real.
0 comentarios