A fondo con Flows & Channels — Parte 5: StateFlows

Julián Ezequiel Picó
Droid LATAM
Published in
5 min readJul 13, 2020

English version

El texto está basado en la versión 1.3.7 de la biblioteca kotlinx.coroutines

Bienvenidos al último (por ahora, al menos) artículo de esta serie, donde vamos a discutir sobre qué son los StateFlows y su comportamiento.

Pueden ver algunos ejemplos en el GitHub Repository. Allí, van a ver como se implementa un StateFlow y como funciona. Ahora que ya tenemos una base sobre la API de Kotlin Streams, van a poder forkear y modificar todos los ejemplos que están ahí 😃

Recuerden que este artículo pertenece a una serie de los mismos:

Empecemos con la parte cinco: StateFlows!

StateFlows

Qué es un StateFlow?

Ya sabemos que son los Flows, Channels y BroadcastChannels, y con estos ya tenemos todos los tipos de stream implementados. Entonces, qué son los StateFlows?

Bueno, básicamente es una nueva implementación de flow, que está diseñada para hacer un flow stateful, para poder mantener un estado actualizable a través del tiempo. Con esto, decimos que pueden tener un estado y que no dependen de un contexto específico, como si lo tienen las implementaciones normales de flow.

Un StateFlow, como un Flow, es una interfaz. Además, contamos con una versión mutable de StateFlow, representada por la interfaz MutableStateFlow.

Podemos ver que en realidad un StateFlow es un Flow con un valor. El MutableStateFlow hace que ese valor sea mutable, pueda modificarse.

Creando StateFlows

Un StateFlow no puede ser creado directamente, ya que es una interfaz y no podemos instanciar interfaces.

En su lugar, existe una factory function (si, factory functions for the win) para crear un MutableStateFlow.

Y eso es todo!

Exponiendo al StateFlow

Una de las principales ventajas de los StateFlows es que podemos exponerlos de dos modos distintos, utilizando una única instancia.

Modo "Reactivo"

Si necesitamos acceder al valor de forma reactiva, podemos exponerlo como un Flow. Entonces, si necesitáramos, por ejemplo, recolectar todos los cambios en el valor del StateFlow, podemos recolectarlos del mismo modo que hacíamos con Flows.

Modo "No Reactivo"

Si necesitamos acceder al valor del StateFlow directamente, de forma no reactiva, lo podemos acceder simplemente utilizando el getter.

Emisión y Actualización del Valor

Como podemos imaginar, una actualización del valor del StateFlow dispara una emisión del Flow.

Cómo notifica el StateFlow a sus suscriptores que el valor fue modificado? Bueno, todas las actualizaciones son realizadas de un modo "Conflated": funciona como un Conflated Channel, donde el valor del buffer es siempre el último que se le envió, y todos los valores que no fueron recibidos por alguien se pierden.

Entonces, un recolector lento va a omitir las actualizaciones que sucedan de forma más rápida que el procesamiento del valor anterior. Pero, como hablamos de una emisión conflated, todos los recolectores van a tener el último valor emitido.

Tal vez ese mecanismo de emisiones es una de las razones por la que los StateFlows van a ser un reemplazo de los ConflatedBroadcastChannels. Emiten como un Conflated Channel, y cualquiera que tenga una referencia a los mismos puede leer su valor, por lo que tenemos un multicast.

Cómo cambiar el valor del StateFlow? Simplemente modificándolo con su setter:

Cuál es la diferencia con Flows?

Hay varias diferencias que podemos mencionar. Las más importantes son:

  • Multicast feature: puede tener más de un recolector, y emitir actualizaciones a todos ellos.
  • Flow no reactivo: podemos acceder al valor del StateFlow sin realizar una recolección. También podemos accederlo tan rápido como necesitemos, porque no tenemos que esperar a la siguiente emisión.
  • Emitir desde cualquier lugar: podemos modificar el valor del StateFlow, utilizando su versión mutable, si contamos con su referencia. Entonces, si tenemos la referencia, podemos emitir, tan simple como eso.
  • Scope: los StateFlows no tienen un contexto de ejecución por si mismos. Si recordamos, el scope de un Flow es el CoroutineScope donde se dispara su ejecución. Acá no tenemos esa limitante (o beneficio?).

Cuál es la diferencia con ConflatedBroadcastChannels?

También acá hay varias diferencias que podemos mencionar, aunque en realidad sean muy similares (ya que, conceptualmente, son lo mismo). En mi opinión, las más importantes son:

  • La API de Flow es más simple de utilizar que la API de Channel, por lo que tenemos un componente que funciona muy parecido a un Channel, pero con una API mucho más simple y poderosa.
  • El StateFlow siempre tiene un valor, no así el Channel. No podemos instanciar un StateFlow sin un valor.
  • Tenemos una separación muy clara entre mutabilidad/inmutabilidad. Con los Channels, cualquiera que tenga su referencia puede enviar valores, pero con los StateFlows podemos exponer su versión inmutable y nadie podrá realizar una emisión (porque no podrán).
  • Los StateFlows emiten basándose en igualdad estructural, y los Channels lo hacen basándose en igualdad referencial. Esto significa que si emitimos el mismo valor (no el mismo objeto), esa emisión va a ser omitida. Para obtener más información sobre como funciona la igualdad, revisen este link.
  • Los StateFlows no pueden ser cerrados o representar una falla, porque su API no lo permite. Esa es una gran diferencia con Channels, donde podíamos cancelarlos o cerrarlos.

Utilizando el StateFlow

No hay mucho para decir acá, ya que hereda de la interfaz de Flow para recolectar los valores emitidos, por lo que podemos utilizarlo como a cualquier Flow. De todos modos, mencionaré algunas cosas que deberían considerar cuando utilicen StateFlows:

  • Un StateFlow tiene un último valor, por lo que siempre vamos a recibir al menos una emisión.
  • La emisión de valores puede ser activada antes de que alguien se suscriba al StateFlow.
  • Tenemos un stream multicast.

Y eso es todo para StateFlows!

Conclusiones

Luego de cinco artículos, no quiero perder la oportunidad de dejar algunas opiniones sobre las distintas APIs y cuáles de ellas es más conveniente utilizar.

Primero, creo que estas APIs son muy útiles para la gran mayoría de los casos de uso que requieran flujos reactivos. Con todos los componentes de los que hablamos, podemos estar seguros de que si queremos, podemos implementar cualquier flujo reactivo en nuestras aplicaciones o sistemas. Entonces, muy bueno para nosotros poder depender solo del lenguaje de programación para hacer todo esto!

Segundo, creo que ya notaron que no hay una separación tan definida entre streams hot y cold. Adicionalmente, en mi opinión, no es tan importante saber si uno tiene que elegir un stream cold o hot, en su lugar, tenemos que saber cómo funcionan las APIs de Kotlin y luego decidir cuál nos va a ayudar a cumplir nuestro objetivo. Las cosas más importantes que tenemos que entender es cuáles son sus propósitos y sus comportamientos, no las definiciones.

Por último, si estás desarrollando para la plataforma Android, te recomiendo que evites la API de Channels. Se puede utilizar Flow y StateFlow para desarrollar casi cualquier feature, y son mucho más simples de utilizar que los Channels. Entonces, si estás pensando en implementar algún feature de Channels, asegurate de que lo que necesites no sea un StateFlow.

El fin

Bueno, ya estamos en la última sección del último artículo. Gracias a todos por leer, compartir y clappear los artículos de esta serie.

Realmente disfrute escribirlos, y realmente espero que hayan sido útiles para todos los desarrolladores que están ingresando en el mundo de los streams.

Voy a intentar estar al día con las nuevas versiones de la biblioteca kotlinx coroutines, y así ir actualizando los artículos de forma adecuada.

Gracias nuevamente y nos leemos!

Special thanks to the MediaMonks Android team that gives feedback.

--

--