Kotlin: Flow

Revisión de los flujos de datos en Android Kotlin

Antes de meterse en temas de StateFlow o LiveData creo que merece la pena dar un repaso a lo que es un Flow. Podemos definir Flow como un tipo que emite datos de forma secuencial dentro de un Coroutines (Veremos, si tenemos tiempo, que son las Coroutines en kotlin).  

Según la documentación de google con respecto a Flow y Kotlin nos dice que un flujo es similar a un Iterator pero con funciones suspendidas. Una función o método suspend es similar a un .await que podemos usar lenguajes async. El caso es que este Flow nos permite la transmisión de datos sin generar bloqueos en el subproceso principal.

Para poder tener este flujo de datos necesitamos, como mínimo, un productor y un consumidor. Opcionalmente, se puede tener también una tercera "pata" que sería el intermediario, suele usarse para la modificación datos del flujo o el propio flujo en si. Para nuestro caso y explicación no lo vamos a necesitar por ahora.

Lo más normal es que un productor sea un repositorio de datos que tengamos definido en nuestra Data Layer. Desde la IU de nuestro consumidor se generan los eventos o se consumen los datos directamente que produce el productor. Todo esto está relacionado directamente con la arquitectura o modelo con el que vamos a construir nuestra aplicación, normalmente usaremos el modelo recomendado por Android que es MVVM (Model, View, View Model).

Vamos a ver ahora como crear, modificar y obtener datos usando Flow y Coroutines en Kotlin. Todos estos ejemplos, como siempre, están en el repositorio de Github.

Se puede usar flow del api para crear un nuevo flujo que se puede emitir de forma manual con la función emit.

En nuestro ejemplo podemos ver como obtenemos un listado de peliculas a un intervalo fijo:

class RemoteMovieDadaSource(private val webService: WebService) {
    val getMovieList : Flow<MovieList> = flow {
        while (true) {
            val movies =
                webService.getMovieList(lang = AppConstants.LANG, apiKey = AppConstants.API_KEY)
            emit(movies)
            delay(5000)
        }
    }
}

Flow se está ejecutando dentro de las couroutines (suspend fun) y aunque se consideran peticiones asíncronas tienen limitaciones:

  • La emision de datos solo se produce cuando se completa el flujo de la función suspend que realiza la petición de red .
  • No se pueden emitir valores a un flujo o cambiarlos desde un CouriteContext diferente. 

¿Cómo se modifica un flow?

Los intermediarios pueden usar operadores intermedios para modificar el flujo de datos sin consumir los valores. Estos operadores son funciones que, cuando se aplican a un flujo de datos, establecen una cadena de operaciones que no se ejecutan hasta que los valores se consumen en el futuro. Obtén más información sobre los operadores intermedios en la documentación de referencia sobre flujos.

Como comentabamos en parrafos anteriores, existen la figura del intermediario para modificar el flujo o los datos del mismo. Estos, cuando se aplican, crean unas operaciones que no se aplican hasta que se consume el flujo.

Podemos transformar los datos que se mostrarán usando un map:

class MovieRepositoryImpl(private val dataSourceRemote: RemoteMovieDadaSource): MovieRepository
{
    override suspend fun getMovieList(): Flow<MovieList> {
        return dataSourceRemote.getMovieList.onEach {
            it.results.map { movie ->
                movie.title = "Modificando el flow: "+movie.title
            }
        }
    }
}

Los operadores intermedios se pueden aplicar uno después de otro para una cadena de operaciones que se ejecutan de forma diferida cuando se emite un elemento en el flujo. Ten en cuenta que aplicar un operador intermedio en un flujo no inicia la recopilación de flujo.

Podemos encadenar varios intermediarios para modificar los datos del flujo que no se aplican hasta que se emite el flujo (Como comentamos antes, la emisión se realiza cuando se completa la petición). Aplicar un intermediario no afecta a la recolección de los datos.

Para obtener los datos que se emiten en el flujo podemos usar collect. Collect, al ser una suspend fun, tiene que ejecutarse dentro de un Corutine y toma un valor lamda como parametro.

Podemos consumir los datos del flujo de la siguiente forma:

class MovieViewModel(private val repo: MovieRepository) : ViewModel() {
    private val _state = MutableStateFlow(MovieViewModelUiState())
    val state: StateFlow<MovieViewModelUiState>
        get() = _state

    init {
        viewModelScope.launch {
            _state.value = MovieViewModelUiState(
                loading = true,
            )
            try {
                val popularFlow = repo.getMovieList()
                popularFlow.collect { popular ->
                    _state.value = MovieViewModelUiState(
                        loading = false,
                        popular = popular,

                        )
                }
            } catch (e: Exception) {
                Log.d("Main view Model", "${e.message}")
            }
        }
    }
}

 

Mientras el bucle del flow permanece activo y esté emitiendo, se recopilarán los datos en el collect. El flujo de datos se termina cuando se borre el viewModel.

La recopilación se detendrá en los siguientes casos:

  • Se cancela la coroutine (viewModelScople.launch)
  • El productor termina de emitir información y se cierra el flujo de datos.

También te puede interesar: