Usar o estado do Compose fora da interface

O Jetpack Compose State<T> e MutableState<T> são armazenadores de valores de uso geral apoiados pelo sistema de snapshot do Compose. Embora sejam usados com frequência para impulsionar a recomposição da IU, o sistema de snapshots em si não está vinculado à renderização da IU.

Com as bibliotecas AppState (Transform e StateStore), é possível armazenar, derivar, observar e manter o State do Compose em ViewModels, gerenciadores, repositórios e serviços em segundo plano.

Por que usar o estado do Compose além da interface?

Quando você lê o State do Compose em um bloco de observação de estado, como uma função combinável de interface, transform ou derivedStateOf, o ambiente de execução do Compose oferece dois benefícios principais em qualquer camada da arquitetura:

  • Rastreamento de dependência automático e dinâmico: ler o .value de um estado (ou usar a delegação de propriedade by) registra automaticamente esse estado como uma dependência do bloco. Como o rastreamento é dinâmico, as dependências são determinadas no momento da execução pelos caminhos de código executados. Se uma ramificação condicional não for usada (por exemplo, após uma verificação if ou return antecipada), os estados dentro dessa ramificação não serão lidos, e as mudanças neles não vão acionar novos cálculos desnecessários.
  • Consistência e granularidade: é possível declarar propriedades MutableState separadas e detalhadas e agrupar gravações de várias propriedades em Snapshot.withMutableSnapshot. As atualizações intermediárias são isoladas no snapshot mutável e aplicadas de forma atômica quando o bloco é concluído. Assim, os observadores que fazem leituras em um snapshot nunca veem um estado intermediário inconsistente e só fazem um novo cálculo quando as propriedades específicas que eles leem mudam. Ao contrário de StateFlow, não é necessário escolher entre um único StateFlow<UiState> monolítico (que invalida todos os coletores quando qualquer campo muda) e propriedades StateFlow separadas (que não podem ser atualizadas juntas em uma única transação atômica).

Escolher a API de estado certa para sua arquitetura

Escolha como manter e persistir o State do Compose com base em quanto tempo um estado precisa durar e quais componentes precisam acessá-lo:

Categoria do estado Ciclo de vida e escopo Exemplos APIs recomendadas do Compose State
Estado ativo Sempre armazenados na memória, consultados na inicialização e não mantidos após o encerramento do processo. Status da conexão de rede, status da conexão Bluetooth, estado temporário da interface, como seleção de itens.
Estado temporário da interface Sobrevive a mudanças de configuração e ao encerramento do processo iniciado pelo sistema, não é armazenado indefinidamente; tem escopo para uma única tela ou atividade. Elementos da interface expandidos ou recolhidos, posição de rolagem, consulta de pesquisa na tela ou alternâncias de filtro.
Estado da sessão temporária Sobrevive ao encerramento do processo em segundo plano, não é armazenado indefinidamente, é compartilhado entre atividades, serviços ou widgets. Timer de foco ativo, carrinho de compras com várias etapas ou rascunho de formulário em andamento.
  • StateStore encapsulado em um gerenciador ou repositório
  • Chaves que implementam PersistToDataStore e são sincronizadas usando syncToDataStore
Estado permanente Mantidos no disco por tempo indeterminado. Preferências e configurações do usuário (DataStore), dados do app estruturados e bibliotecas de mídia (Room). Fluxos do DataStore ou do Room coletados dentro de transform usando collectAsState

Usar o estado do Compose em um ViewModel

Em um ViewModel, é possível declarar entradas de tela independentes como propriedades MutableState granulares, mantê-las após o encerramento do processo com SavedStateHandle.saveable, atualizar várias propriedades de forma atômica com Snapshot.withMutableSnapshot e combiná-las com streams Flow do repositório em um transform:

sealed interface TaskListState {
    data object Loading : TaskListState
    data class Success(
        val tasks: List<Task>,
        val selectedTaskIds: Set<String>,
        val isOnline: Boolean = true,
    ) : TaskListState
}

@HiltViewModel
class TaskListViewModel @Inject constructor(
    private val tasksRepository: TasksRepository,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {

    var selectedTaskIds by mutableStateOf(emptySet<String>())
        private set

    var searchQuery by savedStateHandle.saveable { mutableStateOf("") }
        private set

    fun updateSearchQuery(query: String) {
        Snapshot.withMutableSnapshot {
            searchQuery = query
            selectedTaskIds = emptySet()
        }
    }

    val tasksState: State<TaskListState> = transform<TaskListState>(
        initialValue = TaskListState.Loading,
        scope = viewModelScope,
    ) {
        val tasks = remember(searchQuery) {
            tasksRepository.getTasks(searchQuery)
        }.collectAsState(initial = null).value
            ?: return@transform TaskListState.Loading

        TaskListState.Success(
            tasks = tasks,
            selectedTaskIds = selectedTaskIds,
        )
    }
}

Padrões-chave neste ViewModel:

  • Use SavedStateHandle.saveable para o estado salvo reativo: delegar searchQuery para SavedStateHandle.saveable com mutableStateOf (de androidx.lifecycle:lifecycle-viewmodel-compose) mantém o valor durante o encerramento do processo como um MutableState, para que o transform rastreie as leituras automaticamente. Evite o SavedStateHandle.saved simples, que retorna uma propriedade Kotlin simples, em vez de State do Compose, e não aciona a recomposição quando alterado.
  • Gravações atômicas de várias propriedades com Snapshot.withMutableSnapshot: atualizar searchQuery e selectedTaskIds dentro de Snapshot.withMutableSnapshot isola as gravações e confirma as duas mudanças em uma única transação de snapshot atômico. Assim, os observadores (incluindo transform) veem as duas atualizações juntas em uma passagem de recomposição, em vez de observar o novo searchQuery pareado com o selectedTaskIds desatualizado.
  • Reutilizar fluxos em recomposições com remember(key): envolver tasksRepository.getTasks(searchQuery) em remember com searchQuery como chave garante que, quando selectedTaskIds mudar e recompuser o bloco transform, o Compose reutilize o Flow do banco de dados em vez de reiniciar a consulta.

Compartilhar o estado do sistema em tempo real no app

Para o estado do sistema em tempo real compartilhado entre vários ViewModels, atividades ou serviços, como o status de conexão de rede ou Bluetooth, não é recomendável manter o status desatualizado no disco após o encerramento do processo. Em vez disso, consulte o estado atual do sistema na inicialização, mantenha-o em uma propriedade mutableStateOf particular dentro de um singleton com escopo de aplicativo e exponha um State<T> somente leitura:

interface NetworkMonitor {
    val isOnline: State<Boolean>
}

@Singleton
class LiveNetworkMonitor @Inject constructor(
    @ApplicationContext context: Context,
) : NetworkMonitor {
    private val connectivityManager =
        context.getSystemService(ConnectivityManager::class.java)

    private val _isOnline = mutableStateOf(checkCurrentConnectivity())
    override val isOnline: State<Boolean> = _isOnline

    private val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities,
        ) {
            _isOnline.value = networkCapabilities.hasCapability(
                NetworkCapabilities.NET_CAPABILITY_INTERNET
            )
        }

        override fun onLost(network: Network) {
            _isOnline.value = false
        }
    }

    init {
        connectivityManager.registerDefaultNetworkCallback(callback)
    }

    private fun checkCurrentConnectivity(): Boolean {
        val network = connectivityManager.activeNetwork ?: return false
        val capabilities =
            connectivityManager.getNetworkCapabilities(network) ?: return false
        return capabilities.hasCapability(
            NetworkCapabilities.NET_CAPABILITY_INTERNET
        )
    }
}

O MutableState oferece suporte a leituras e atualizações consistentes de qualquer linha de execução. Assim, o NetworkCallback pode atualizar o _isOnline.value diretamente da linha de execução de callback em segundo plano. Se um callback em segundo plano atualizar várias propriedades MutableState juntas, envolva as gravações em Snapshot.withMutableSnapshot para que elas sejam aplicadas de forma atômica.

Qualquer componente no app pode ler networkMonitor.isOnline.value, seja como uma verificação única, dentro de um elemento combinável de interface ou dentro de um bloco de observação de estado, como listener, derivedStateOf ou transform. Por exemplo, TaskListViewModel pode ler networkMonitor.isOnline.value dentro do bloco transform:

val tasksState: State<TaskListState> = transform<TaskListState>(
    initialValue = TaskListState.Loading,
    scope = viewModelScope,
) {
    val tasks = remember(searchQuery) {
        tasksRepository.getTasks(searchQuery)
    }.collectAsState(initial = null).value
        ?: return@transform TaskListState.Loading

    TaskListState.Success(
        tasks = tasks,
        selectedTaskIds = selectedTaskIds,
        isOnline = networkMonitor.isOnline.value,
    )
}

Como o transform rastreia networkMonitor.isOnline.value com searchQuery, selectedTaskIds e o repositório Flow, qualquer mudança na conectividade recalcula automaticamente TaskListState.Success sem consultar novamente o banco de dados.