StateFlow y SharedFlow

StateFlow y SharedFlow son API de Flow que permiten que los flujos emitan actualizaciones de estado y valores a varios consumidores de manera óptima.

StateFlow

StateFlow es un flujo observable contenedor de estados que emite actualizaciones de estado actuales y nuevas a sus recopiladores. El valor de estado actual también se puede leer a través de su propiedad value. Para actualizar el estado y enviarlo al flujo, asigna un nuevo valor a la propiedad value de la clase MutableStateFlow.

En Android, StateFlow es una excelente opción para clases que necesitan mantener un estado observable que muta.

De acuerdo con los ejemplos de los flujos de Kotlin, se puede exponer un StateFlow del LatestNewsViewModel para que View pueda detectar actualizaciones de estado de la IU y, de manera inherente, permitir que el estado de la pantalla se conserve después de hacer cambios en la configuración.

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    // Backing property to avoid state updates from other classes
    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
    // The UI collects from this StateFlow to get its state updates
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                // Update View with the latest favorite news
                // Writes to the value property of MutableStateFlow,
                // adding a new element to the flow and updating all
                // of its collectors
                .collect { favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
        }
    }
}

// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
    data class Success(news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(exception: Throwable): LatestNewsUiState()
}

La clase responsable de actualizar un MutableStateFlow es el productor, mientras que todas las clases que se recopilan de StateFlow son consumidores. A diferencia de un flujo frío compilado con el compilador de flow, un StateFlow es caliente; recopilar datos del flujo no activa ningún código de productor. Un objeto StateFlow siempre se encuentra activo y en la memoria, y se vuelve apto para la recolección de elementos no utilizados solo cuando no hay otras referencias a él en la raíz de otra recolección.

Cuando un consumidor nuevo comienza a recopilarse desde el flujo, recibe el último estado del flujo y todos los estados posteriores. Puedes encontrar este comportamiento en otras clases observables, como LiveData.

El View detecta un StateFlow de la misma manera que cualquier otro flujo:

class LatestNewsActivity : AppCompatActivity() {
    private val latestNewsViewModel = // getViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // This coroutine will run the given block when the lifecycle
        // is at least in the Started state and will suspend when
        // the view moves to the Stopped state
        lifecycleScope.launchWhenStarted {
            // Triggers the flow and starts listening for values
            latestNewsViewModel.uiState.collect { uiState ->
                // New value received
                when (uiState) {
                    is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                    is LatestNewsUiState.Error -> showError(uiState.exception)
                }
            }
        }
    }
}

Para convertir cualquier flujo en un StateFlow, usa el operador intermedio stateIn.

StateFlow, Flow y LiveData

StateFlow y LiveData tienen similitudes. Ambas son clases contenedoras de datos observables y siguen un patrón similar cuando se usan en la arquitectura de tu app.

Sin embargo, ten en cuenta que StateFlow y LiveData se comportan de manera diferente:

  • StateFlow requiere que se pase un estado inicial al constructor, mientras que LiveData, no.
  • LiveData.observe() cancela automáticamente el registro del consumidor cuando la vista pasa al estado STOPPED, mientras que la recopilación de StateFlow o cualquier otro flujo, no.

En el ejemplo anterior que usó launchWhenStarted para recopilar el flujo, cuando la corrutina que activa la recopilación de flujos se suspende una vez que View pasa a segundo plano, los productores subyacentes permanecen activos.

En el caso de implementaciones en caliente, ten cuidado durante la recopilación cuando la IU no se muestre en pantalla, ya que podrían desperdiciarse recursos. En su lugar, puedes dejar de recopilar el flujo de forma manual, como se muestra en el siguiente ejemplo:

class LatestNewsActivity : AppCompatActivity() {
    ...
    // Coroutine listening for UI states
    private var uiStateJob: Job? = null

    override fun onStart() {
        super.onStart()
        // Start collecting when the View is visible
        uiStateJob = lifecycleScope.launch {
            latestNewsViewModel.uiState.collect { uiState -> ... }
        }
    }

    override fun onStop() {
        // Stop collecting when the View goes to the background
        uiStateJob?.cancel()
        super.onStop()
    }
}

Otra forma de detener la detección de los cambios de uiState cuando la vista no es visible es convertir el flujo a LiveData con la función asLiveData() de la biblioteca lifecycle-livedata-ktx:

class LatestNewsActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        latestNewsViewModel.uiState.asLiveData().observe(owner = this) { state ->
            // Handle UI state
        }
    }
}

Cómo convertir flujos fríos en calientes con shareIn

StateFlow es un flujo caliente, es decir, permanece en la memoria siempre que se recopile o mientras que una raíz de recolección de elementos no utilizados origine alguna referencia a él. Puedes usar el operador shareIn para cambiar los flujos fríos a calientes.

Si usas el callbackFlow creado en flujos de Kotlin como ejemplo, en lugar de hacer que cada recopilador cree un flujo nuevo, puedes compartir los datos recuperados de Firestore entre recopiladores con shareIn. Debes pasar lo siguiente:

  • Un CoroutineScope que se use para compartir el flujo (este alcance debe durar más que cualquier consumidor para mantener el flujo compartido activo el tiempo que sea necesario)
  • La cantidad de elementos que se volverán a reproducir en cada recopilador nuevo
  • La política de comportamiento de inicio
class NewsRemoteDataSource(...,
    private val externalScope: CoroutineScope,
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}

En este ejemplo, el flujo latestNews vuelve a reproducir el último elemento emitido en un recopilador nuevo y permanecerá activo mientras externalScope esté activo y haya recopiladores activos. La política de inicio SharingStarted.WhileSubscribed() mantendrá activo el productor ascendente mientras haya suscriptores activos. También hay otras políticas de inicio disponibles, como SharingStarted.Eagerly para iniciar inmediatamente el productor o SharingStarted.Lazily para empezar a compartir después de que aparece el primer suscriptor y mantener el flujo activo de forma permanente.

SharedFlow

La función shareIn muestra un SharedFlow, un flujo caliente que emite valores para todos los consumidores que recopilan datos de él. Un SharedFlow es una generalización que admite una amplia configuración de StateFlow.

Puedes crear un SharedFlow sin usar shareIn. Como ejemplo, puedes usar un SharedFlow para enviar marcas al resto de la app, de modo que todo el contenido se actualice simultáneamente y de manera periódica. Además de recuperar las noticias más recientes, puedes actualizar la sección de información del usuario con su colección de temas favoritos. En el siguiente fragmento de código, un TickHandler expone un elemento SharedFlow para que otras clases sepan cuándo actualizar su contenido. Al igual que con StateFlow, usa una propiedad de copia de seguridad de tipo MutableSharedFlow en una clase para enviar elementos al flujo:

// Class that centralizes when the content of the app needs to be refreshed
class TickHandler(
    private val externalScope: CoroutineScope,
    private val tickIntervalMs: Long = 5000
) {
    // Backing property to avoid flow emissions from other classes
    private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
    val tickFlow: SharedFlow<Event<String>> = _tickFlow

    init {
        externalScope.launch {
            while(true) {
                _tickFlow.emit(Unit)
                delay(tickIntervalMs)
            }
        }
    }
}

class NewsRepository(
    ...,
    private val tickHandler: TickHandler,
    private val externalScope: CoroutineScope
) {
    init {
        externalScope.launch {
            // Listen for tick updates
            tickHandler.tickFlow.collect {
                refreshLatestNews()
            }
        }
    }

    suspend fun refreshLatestNews() { ... }
    ...
}

Puedes personalizar el comportamiento de SharedFlow de las siguientes maneras:

  • replay te permite volver a enviar una serie de valores previamente emitidos para suscriptores nuevos.
  • onBufferOverflow te permite especificar una política para las ocasiones en que el búfer está lleno de elementos que se enviarán. El valor predeterminado es BufferOverflow.SUSPEND, lo que provoca la suspensión del emisor. Otras opciones son DROP_LATEST o DROP_OLDEST.

MutableSharedFlow también tiene una propiedad subscriptionCount, que contiene la cantidad de recopiladores activos para que puedas optimizar tu lógica empresarial según corresponda. MutableSharedFlow también contiene una función resetReplayCache en caso de que no desees volver a reproducir la información más reciente enviada al flujo.

Recursos adicionales de flujo