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
.valuede um estado (ou usar a delegação de propriedadeby) 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çãoifoureturnantecipada), 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
MutableStateseparadas e detalhadas e agrupar gravações de várias propriedades emSnapshot.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 deStateFlow, não é necessário escolher entre um únicoStateFlow<UiState>monolítico (que invalida todos os coletores quando qualquer campo muda) e propriedadesStateFlowseparadas (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. |
|
| 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.saveablepara o estado salvo reativo: delegarsearchQueryparaSavedStateHandle.saveablecommutableStateOf(deandroidx.lifecycle:lifecycle-viewmodel-compose) mantém o valor durante o encerramento do processo como umMutableState, para que otransformrastreie as leituras automaticamente. Evite oSavedStateHandle.savedsimples, que retorna uma propriedade Kotlin simples, em vez deStatedo Compose, e não aciona a recomposição quando alterado. - Gravações atômicas de várias propriedades com
Snapshot.withMutableSnapshot: atualizarsearchQueryeselectedTaskIdsdentro deSnapshot.withMutableSnapshotisola as gravações e confirma as duas mudanças em uma única transação de snapshot atômico. Assim, os observadores (incluindotransform) veem as duas atualizações juntas em uma passagem de recomposição, em vez de observar o novosearchQuerypareado com oselectedTaskIdsdesatualizado. - Reutilizar fluxos em recomposições com
remember(key): envolvertasksRepository.getTasks(searchQuery)emremembercomsearchQuerycomo chave garante que, quandoselectedTaskIdsmudar e recompuser o blocotransform, o Compose reutilize oFlowdo 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.