O Google tem o compromisso de promover a igualdade racial para as comunidades negras. Saiba como.

StateFlow e SharedFlow

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

StateFlow

StateFlow (link em inglês) é um fluxo observável com 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 IU 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(news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(exception: Throwable): LatestNewsUiState()
}

A classe responsável por atualizar um MutableStateFlow é o produtor, e todas as classes coletadas 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. É possível ver esse comportamento em outras classes observáveis, como LiveData.

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

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 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. Ambos são classes observáveis do armazenador de dados e seguem um padrão semelhante quando usados 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 faz isso.

No exemplo anterior, que usou launchWhenStarted para coletar o fluxo, quando a corrotina que aciona a coleta de fluxos é suspensa à medida que View vai para o segundo plano, os produtores subjacentes permanecem ativos.

Com implementações quentes, tenha cuidado ao coletar quando a IU não estiver na tela, porque isso pode desperdiçar recursos. Em vez disso, é possível interromper manualmente a coleta do fluxo, conforme mostrado no exemplo a seguir:

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()
    }
}

Outra maneira de parar de detectar mudanças em uiState quando a visualização não está visível é converter o fluxo para LiveData usando a função asLiveData() da biblioteca lifecycle-livedata-ktx:

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

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 (link 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 está ativo e há coletores ativos. 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 dela. 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 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 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.