StateFlow e SharedFlow

StateFlow e SharedFlow são APIs Flow. Elas permitem que os fluxos emitam atualizações de estado de maneira otimizada e que também emitam valores para vários consumidores.

StateFlow

StateFlow é um fluxo observável detentor de estado que emite as atualizações de estado novas e atuais para os coletores. O valor do estado atual também pode ser lido usando a propriedade value (link em inglês) dele. Para atualizar o estado e enviá-lo ao fluxo, atribua um novo valor à propriedade value da classe MutableStateFlow (link em inglês).

No Android, StateFlow é uma ótima opção para classes que precisam manter um estado mutável observável.

Seguindo os exemplos de fluxos Kotlin, um StateFlow pode ser exposto do LatestNewsViewModel para que View possa detectar as atualizações de estado da interface e, inerentemente, fazer com que o estado da tela sobreviva às mudanças de configuração.

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(val news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(val exception: Throwable): LatestNewsUiState()
}

A classe responsável por atualizar uma MutableStateFlow é o produtor, e todas as classes coletando do StateFlow são os consumidores. Ao contrário de um fluxo frio criado usando o builder flow, um StateFlow é quente: a coleta de dados do fluxo não aciona nenhum código de produtor. Um StateFlow sempre fica ativo e na memória. Ele se torna qualificado para coleta de lixo somente quando não há outras referências a ele em uma raiz da coleta de lixo.

Quando um novo consumidor começa a coletar do fluxo, ele recebe o último estado no stream e os estados subsequentes. Outras classes observáveis, como LiveData, também apresentam esse comportamento.

View detecta StateFlow da mesma forma que faria com qualquer outro fluxo:

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

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Start a coroutine in the lifecycle scope
        lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // Note that this happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                latestNewsViewModel.uiState.collect { uiState ->
                    // New value received
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }
    }
}

Para converter qualquer fluxo em um StateFlow, use o operador intermediário stateIn (link em inglês).

StateFlow, Flow e LiveData

StateFlow e LiveData têm semelhanças. Ambas são classes observáveis do detentor de dados e seguem um padrão semelhante quando usadas na arquitetura do app.

No entanto, observe que StateFlow e LiveData se comportam de maneira diferente:

  • StateFlow necessita que um estado inicial seja transmitido para o construtor, enquanto LiveData não.
  • LiveData.observe() cancela o registro do consumidor automaticamente quando a visualização vai para o estado STOPPED, enquanto a coleta de um StateFlow ou qualquer outro fluxo não para a coleta automaticamente. Para ter o mesmo comportamento, você precisa coletar o fluxo de um bloco Lifecycle.repeatOnLifecycle.

Como converter fluxos frios em quentes com shareIn

StateFlow é um fluxo quente. Ele permanece na memória enquanto o fluxo é coletado ou enquanto qualquer outra referência a ele existe em uma raiz de coleta de lixo. Use o operador shareIn (em inglês) para converter os fluxos frios em quentes.

Usando o callbackFlow criado em fluxos Kotlin como exemplo, em vez de fazer com que cada coletor crie um novo fluxo, é possível compartilhar os dados recuperados do Firestore entre coletores usando shareIn. As seguintes informações precisam ser transmitidas:

  • Um CoroutineScope que é usado para compartilhar o fluxo. O escopo precisa sobreviver mais que qualquer consumidor para manter o fluxo compartilhado ativo pelo tempo necessário.
  • O número de itens a serem repetidos para cada novo coletor.
  • A política de comportamento inicial.
class NewsRemoteDataSource(...,
    private val externalScope: CoroutineScope,
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}

Neste exemplo, o fluxo latestNews reproduz novamente o último item emitido para um novo coletor e permanece ativo enquanto externalScope e coletores também estão. A política de início SharingStarted.WhileSubscribed() mantém o produtor upstream ativo enquanto há inscritos ativos. Outras políticas de início estão disponíveis, como SharingStarted.Eagerly para iniciar o produtor imediatamente ou SharingStarted.Lazily para começar a compartilhar após o primeiro inscrito ser exibido e manter o fluxo ativo para sempre.

SharedFlow

A função shareIn retorna um SharedFlow, um fluxo quente que emite valores para todos os consumidores que coletam dados dele. Um SharedFlow é uma generalização altamente configurável de StateFlow.

Você pode criar um SharedFlow sem usar shareIn. Por exemplo, é possível usar um SharedFlow para enviar marcações ao restante do app para que todo o conteúdo seja atualizado periodicamente ao mesmo tempo. Além de buscar as notícias mais recentes, você também pode atualizar a seção de informações do usuário com a coleção de temas favoritos dele. No snippet de código a seguir, um TickHandler expõe um SharedFlow para que outras classes saibam quando atualizar o conteúdo. Como acontece com o StateFlow, use uma propriedade de backup do tipo MutableSharedFlow em uma classe para enviar itens para o fluxo:

// 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() { ... }
    ...
}

É possível personalizar o comportamento de SharedFlow das seguintes maneiras:

  • replay permite reenviar diversos valores previamente emitidos para novos inscritos.
  • onBufferOverflow permite especificar uma política para quando o buffer estiver cheio de itens a serem enviados. O valor padrão é BufferOverflow.SUSPEND, o que suspende o autor da chamada. Outras opções são DROP_LATEST ou DROP_OLDEST.

MutableSharedFlow também tem uma propriedade subscriptionCount que contém o número de coletores ativos para que você possa otimizar sua lógica de negócios de acordo com essa informação. MutableSharedFlow também contém uma função resetReplayCache se você não quiser repetir as informações mais recentes enviadas ao fluxo.

Recursos de fluxo adicionais