O estado em um app é qualquer valor que pode mudar ao longo do tempo. Essa é uma definição muito ampla e abrange tudo, de um banco de dados da Room até a variável em uma classe.
Todos os apps Android exibem o estado para o usuário. Alguns exemplos de estado em apps Android:
- Um snackbar que mostra quando não é possível estabelecer uma conexão de rede.
- Uma postagem de blog e comentários associados.
- Animação de ripple em botões que são reproduzidas quando um usuário clica neles.
- Adesivos que podem ser desenhados sobre uma imagem.
O Jetpack Compose ajuda a deixar claro onde e como você armazena e usa o estado em um app Android. Este guia se concentra na conexão entre o estado e os elementos que podem ser compostos, assim como nas APIs que o Jetpack Compose oferece para trabalhar mais facilmente com o estado.
Estado e composição
O Compose é declarativo e, portanto, a única maneira de atualizá-lo é chamando
com novos argumentos o mesmo elemento que pode composto. Esses argumentos são representações do
estado da IU. Sempre que um estado é atualizado, ocorre uma recomposição. Por isso,
itens como o TextField
não são atualizados automaticamente como seriam em
visualizações imperativas baseadas em XML. Um elemento de composição precisa ser explicitamente informado sobre o novo estado
para que seja atualizado corretamente.
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("Name") }
)
}
}
Se você executar esse código, verá que nada acontece. Isso ocorre porque o TextField
não atualiza a si mesmo. Ele é atualizado quando o parâmetro value
muda. Isso se deve
à maneira como a composição e a recombinação funcionam no Compose.
Para saber mais sobre a composição inicial e a recomposição, consulte Trabalhando com o Compose.
Estado dos elementos que podem ser compostos
As funções que podem ser compostas podem armazenar um único objeto na memória usando o
elemento remember
. Um valor calculado por remember
é armazenado durante
a composição inicial, e o valor armazenado é retornado durante a recomposição.
O remember
pode ser usado para armazenar objetos mutáveis e imutáveis.
A função mutableStateOf
cria um
MutableState<T>
observável, que é integrado ao ambiente de execução do Compose.
interface MutableState<T> : State<T> {
override var value: T
}
Qualquer mudança em value
programará a recomposição de qualquer função que pode ser composta
que leia value
. No caso de ExpandingCard
, sempre que expanded
é mudado,
isso faz com que ExpandingCard
seja recomposto.
Há três maneiras de declarar um objeto MutableState
em um elemento que pode ser composto:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
Essas declarações são equivalentes e são fornecidas como açúcar de sintaxe para diferentes usos do estado. Escolha aquela que produz o código mais fácil de ler no elemento de composição que você está criando.
A sintaxe by
delegada requer estas importações:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
É possível usar o valor salvo como parâmetro para outros elementos de composição ou mesmo como
lógica em instruções para mudar quais desses elementos serão mostrados. Por exemplo, se
você não quiser exibir a saudação se o nome estiver vazio, use o estado em uma instrução
if
:
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
Embora remember
ajude a manter o estado em recomposições, o estado não é
mantido em todas as mudanças de configuração. Para isso, use
rememberSaveable
. O rememberSaveable
salva automaticamente qualquer valor que possa ser
salvo em um Bundle
. Para outros valores, é possível transmitir um objeto de economia personalizado.
Outros tipos de estado compatíveis
O Jetpack Compose não exige que você use MutableState<T>
para manter o estado.
Ele é compatível com outros tipos observáveis. Antes de ler outro
tipo observável no Jetpack Compose, você precisa convertê-lo em um State<T>
para que o
Jetpack Compose possa fazer automaticamente a recomposição quando o estado for modificado.
O Compose é enviado com funções para criar State<T>
a partir de tipos observáveis comuns
usados em apps Android:
Você pode criar uma função de extensão para o Jetpack Compose para ler outros tipos
observáveis se o app usar uma classe observável personalizada. Consulte a implementação dos
builtins para ver exemplos de como fazer isso. Qualquer objeto que permita que o Jetpack Compose
faça a inscrição em todas as mudanças pode ser convertido em State<T>
e lido por um
elemento que pode ser composto.
Com estado X sem estado
Um elemento que pode ser composto que usa o remember
para armazenar um objeto cria um estado interno,
transformando o elemento em com estado. O HelloContent
é um exemplo de elemento
com estado porque mantém e modifica internamente o estado de name
. Isso pode
ser útil em situações em que um autor de chamada não precisa controlar o estado e pode
usá-lo sem ter que gerenciar o estado por conta própria. No entanto, os elementos que podem ser compostos que têm
estado interno tendem a ser menos reutilizáveis e mais difíceis de testar.
Um elemento que pode ser composto sem estado é um elemento que não tem um estado. Uma maneira fácil de ficar sem estado é usando a elevação de estado.
Ao desenvolver elementos que podem ser compostos reutilizáveis, frequentemente você quer expor uma versão com estado e uma sem estado do mesmo elemento. A versão com estado é conveniente para autores de chamada que não se importam com ele, e a sem estado é necessária para autores de chamada que precisam controlar ou elevar o estado.
Elevação de estado
A elevação de estado no Compose é um padrão para mover um estado para o autor da chamada de modo a criar um elemento que pode ser composto sem estado. O padrão geral para elevação de estado no Jetpack Compose é substituir a variável por dois parâmetros:
value: T
: o valor atual a ser exibido.onValueChange: (T) -> Unit
: um evento que solicita a mudança do valor, em queT
é o novo valor proposto.
No entanto, você não se limita a onValueChange
. Se eventos mais específicos forem
apropriados para o elemento que pode ser composto, defina-os usando lambdas da mesma forma que
ExpandingCard
faz com onExpand
e onCollapse
.
O estado elevado dessa maneira tem algumas propriedades importantes:
- Única fonte da verdade: ao mover o estado em vez de duplicá-lo, garantimos que exista apenas uma fonte de verdade. Isso ajuda a evitar bugs.
- Encapsulado: somente elementos que podem ser compostos com estado poderão modificar esse estado. Ele é totalmente interno.
- Compartilhável: o estado elevado pode ser compartilhado com vários elementos que podem ser compostos. Caso
quiséssemos usar
name
em um tipo diferente de elemento que pode ser composto, por exemplo, a elevação permitiria isso. - Interceptável: os autores de chamada para elementos que podem ser compostos sem estado podem decidir ignorar ou modificar eventos antes de mudar o estado.
- Dissociado: o estado do
ExpandingCard
sem estado pode ser armazenado em qualquer lugar. Por exemplo, agora é possível mover oname
para umViewModel
.
No exemplo, o name
e o onValueChange
são extraídos de
HelloContent
e movidos para cima na árvore até um elemento HelloScreen
que pode ser composto
e que chama o HelloContent
.
@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
Ao elevar o estado do HelloContent
, é mais fácil entender o
elemento que pode ser composto, reutilizá-lo em situações diferentes e testá-lo. O HelloContent
está
dissociado do modo como o estado é armazenado. Isso significa que, se você modificar ou
substituir HelloScreen
, não precisará mudar a forma como o HelloContent
é
implementado.

O padrão em que o estado desce e os eventos sobem é chamado de
fluxo de dados unidirecional. Nesse caso, o estado desce de HelloScreen
para HelloContent
e os eventos sobem de HelloContent
para HelloScreen
. Ao
seguir o fluxo de dados unidirecional, você pode dissociar os elementos que exibem
o estado na IU das partes do app que armazenam e mudam o estado.
Como restaurar o estado no Compose
Use o rememberSaveable
para restaurar o estado da IU após a recriação de uma atividade ou
de um processo. O rememberSaveable
mantém o estado nas recomposições.
Além disso, ele também mantém o estado
nas recriações de atividades e de processos.
Formas de armazenar o estado
Todos os tipos de dados adicionados ao Bundle
são salvos automaticamente. Caso você
queira salvar algo que não possa ser adicionado ao Bundle
, há várias
opções.
Parcelize
A solução mais simples é adicionar a anotação
@Parcelize
(link em inglês)
ao objeto. O objeto se tornará parcelable e poderá ser empacotado. Por
exemplo, esse código cria um tipo de dado parcelable City
e o salva no
estado.
@Parcelize
data class City(val name: String, val country: String) : Parcelable
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
MapSaver
Se, por algum motivo, o @Parcelize
não for adequado, use o mapSaver
para
definir sua própria regra de conversão de um objeto em um conjunto de valores que o
sistema poderá salvar no Bundle
.
data class City(val name: String, val country: String)
val CitySaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name, countryKey to it.country) },
restore = { City(it[nameKey] as String, it[countryKey] as String) }
)
}
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
ListSaver
Para evitar a necessidade de definir as chaves do mapa, você também pode usar listSaver
e usar seus índices como chaves:
data class City(val name: String, val country: String)
val CitySaver = listSaver<City, Any>(
save = { listOf(it.name, it.country) },
restore = { City(it[0] as String, it[1] as String) }
)
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
Como gerenciar o estado no Compose
A elevação de estado simples pode ser gerenciada nas próprias funções que podem ser compostas. No entanto, caso a quantidade de estados para gerenciar aumente ou surja uma lógica para realizar em funções que podem ser compostas, é recomendável delegar as responsabilidades de lógica e estado a outras classes: detentores de estado.
Esta seção aborda como gerenciar o estado de várias maneiras no Compose. Dependendo da complexidade do elemento que pode ser composto, há diferentes alternativas para considerar:
- Elementos que podem ser compostos no gerenciamento simples de estado do elemento da IU.
- Detentores de estado no gerenciamento complexo de estado do elemento da IU. Eles são detentores do estado e da lógica dos elementos da IU.
- Os ViewModels dos componentes da arquitetura são um tipo especial de detentores de estado responsáveis por fornecer acesso à lógica de negócios e ao estado da tela ou da IU.
Os detentores de estado têm vários tamanhos, dependendo do escopo dos elementos da IU correspondentes que gerenciam, variando de um único widget, como uma barra inferior de apps, à tela inteira. Os detentores de estado são agrupáveis, ou seja, um detentor de estado pode ser integrado a outro, principalmente ao agregar estados.
O diagrama a seguir mostra um resumo das relações entre as entidades envolvidas no gerenciamento de estado do Compose. O restante da seção abrange cada entidade em detalhes:
- Um elemento que pode ser composto pode depender de 0 ou mais detentores de estado (que podem ser objetos simples, ViewModels ou ambos), dependendo da complexidade.
- Um detentor de estado simples poderá depender de um ViewModel se precisar de acesso à lógica de negócios ou ao estado da tela.
- Um ViewModel depende das camadas de negócios ou de dados.
Resumo das dependências (opcionais) para cada entidade envolvida no gerenciamento de estado do Compose.
Tipos de estado e lógica
Em um app Android, há diferentes tipos de estado a considerar:
O estado do elemento da IU é o estado elevado desses elementos. Por exemplo,
ScaffoldState
processa o estado do elementoScaffold
que pode ser composto.O estado da tela ou IU é o que precisa ser exibido na tela. Por exemplo, uma classe
CartUiState
que pode conter itens do carrinho, mensagens a serem exibidas ao usuário ou carregamento de sinalizações. Esse estado geralmente está conectado a outras camadas da hierarquia, já que contém dados do aplicativo.
Além disso, existem diferentes tipos de lógica:
A lógica do comportamento da IU ou lógica da IU está relacionada a como exibir mudanças de estado na tela. Por exemplo, a lógica de navegação decide qual tela exibir em seguida, ou então a lógica da IU decide como exibir mensagens do usuário na tela com snackbars ou avisos. A lógica do comportamento da IU precisa estar sempre na composição.
A lógica de negócios informa o que fazer com as mudanças de estado. Por exemplo, fazer um pagamento ou armazenar preferências do usuário. Essa lógica geralmente é colocada nas camadas de negócios ou de dados, nunca na camada da IU.
Elementos que podem ser compostos como fonte da verdade
Ter uma lógica da IU e um estado de elementos da IU entre os elementos que podem ser compostos será uma boa abordagem se
ambos forem simples. Por exemplo, veja o processamento de ScaffoldState
e CoroutineScope
pelo
elemento MyApp
que pode ser composto.
@Composable
fun MyApp() {
MyTheme {
val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
MyContent(
showSnackbar = { message ->
coroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(message)
}
}
)
}
}
}
Como ScaffoldState
contém propriedades mutáveis, todas as interações com ele
precisam acontecer no elemento MyApp
que pode ser composto. Caso contrário, se ele for transmitido para outros
elementos que podem ser compostos,
eles poderão modificar o estado dele, o que não está de acordo com a única fonte do
princípio da verdade e dificulta o rastreamento de bugs.
Detentores de estado como fonte da verdade
Quando um elemento que pode ser composto contém uma lógica da IU complexa que envolve o estado de vários elementos da IU, ele precisa delegar essa responsabilidade aos detentores de estado. Isso faz com que essa lógica seja mais testável em isolamento e reduz a complexidade do elemento. Essa abordagem favorece o princípio de separação de conceitos: o elemento que pode ser composto é responsável por emitir elementos da IU, e o detentor do estado contém a lógica da IU e o estado de elementos da IU.
Os detentores de estado são classes simples criadas e lembradas na composição. Como eles seguem o ciclo de vida dos elementos que podem ser compostos, podem usar dependências do Compose.
Se o elemento MyApp
que pode ser composto da seção Elementos que podem ser compostos como fonte da verdade passar a ter mais responsabilidades, será possível criar
um detentor de estado MyAppState
para gerenciar a complexidade dele:
// Plain class that manages App's UI logic and UI elements' state
class MyAppState(
val scaffoldState: ScaffoldState,
val navController: NavHostController,
private val resources: Resources,
/* ... */
) {
val bottomBarTabs = /* State */
// Logic to decide when to show the bottom bar
val shouldShowBottomBar: Boolean
get() = /* ... */
// Navigation logic, which is a type of UI logic
fun navigateToBottomBarRoute(route: String) { /* ... */ }
// Show snackbar using Resources
fun showSnackbar(message: String) { /* ... */ }
}
@Composable
fun rememberMyAppState(
scaffoldState: ScaffoldState = rememberScaffoldState(),
navController: NavHostController = rememberNavController(),
resources: Resources = LocalContext.current.resources,
/* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
MyAppState(scaffoldState, navController, resources, /* ... */)
}
Como MyAppState
usa dependências, é recomendável fornecer um
método que se lembre de uma instância de MyAppState
na composição. Nesse
caso, a função rememberMyAppState
.
Agora, o foco de MyApp
é a emissão de elementos da IU, delegando toda a lógica da IU
e o estado dos elementos da IU a MyAppState
:
@Composable
fun MyApp() {
MyTheme {
val myAppState = rememberMyAppState()
Scaffold(
scaffoldState = myAppState.scaffoldState,
bottomBar = {
if (myAppState.shouldShowBottomBar) {
BottomBar(
tabs = myAppState.bottomBarTabs,
navigateToRoute = {
myAppState.navigateToBottomBarRoute(it)
}
)
}
}
) {
NavHost(navController = myAppState.navController, "initial") { /* ... */ }
}
}
}
Como você pode ver, incrementar as responsabilidades de um elemento que pode ser composto aumenta a necessidade de um detentor de estado. As responsabilidades podem estar na lógica da IU ou apenas na quantidade de estados para gerenciar.
ViewModels como fonte da verdade
Se as classes de detentores de estado simples forem responsáveis pela lógica da IU e pelo estado dos elementos da IU, um ViewModel será um tipo especial de detentor de estado responsável por:
- fornecer acesso à lógica de negócios do aplicativo, que normalmente é colocada em outras camadas da hierarquia, como as camadas de negócios e de dados;
- preparar os dados do aplicativo para a apresentação em uma tela específica, que se torna o estado da tela ou da IU.
Os ViewModels têm um ciclo de vida mais longo que o da composição porque sobrevivem a mudanças de configuração. Eles podem seguir o ciclo de vida do host do conteúdo do Compose, ou seja, atividades ou fragmentos, ou o ciclo de vida de um destino ou gráfico de navegação, caso você esteja usando a biblioteca Navigation. Devido ao ciclo de vida mais longo, os ViewModels não podem conter referências ao estado de duração longa vinculadas ao ciclo de vida da composição. Se isso acontecer, eles poderão causar vazamentos de memória.
Recomendamos que os elementos de composição no nível da tela usem instâncias do ViewModel para fornecer acesso à lógica de negócios e ser a fonte da verdade para o estado da IU. Não transmita instâncias do ViewModel para outros elementos de composição. Consulte a seção ViewModel e holders de estado para ver por que o ViewModel pode ser usado para isso.
Confira abaixo um exemplo de ViewModel usado em um elemento de composição da tela:
data class ExampleUiState(
val dataToDisplayOnScreen: List<Example> = emptyList(),
val userMessages: List<Message> = emptyList(),
val loading: Boolean = false
)
class ExampleViewModel(
private val repository: MyRepository,
private val savedState: SavedStateHandle
) : ViewModel() {
var uiState by mutableStateOf(ExampleUiState())
private set
// Business logic
fun somethingRelatedToBusinessLogic() { /* ... */ }
}
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState
/* ... */
ExampleReusableComponent(
someData = uiState.dataToDisplayOnScreen,
onDoSomething = { viewModel.somethingRelatedToBusinessLogic() }
)
}
@Composable
fun ExampleReusableComponent(someData: Any, onDoSomething: () -> Unit) {
/* ... */
Button(onClick = onDoSomething) {
Text("Do something")
}
}
ViewModel e detentores de estado
Os benefícios dos ViewModels no desenvolvimento Android os tornam adequados para fornecer acesso à lógica de negócios e preparar os dados do aplicativo para serem apresentados na tela. Estas são as vantagens:
- Operações acionadas por ViewModels sobrevivem a mudanças de configuração.
- Integração com o Navigation:
- O Navigation armazena em cache os ViewModels enquanto a tela está na backstack. Isso é importante para disponibilizar os dados já carregados instantaneamente ao retornar ao destino. É mais difícil fazer isso com um detentor de estado que segue o ciclo de vida da tela que pode ser composta.
- O ViewModel também é apagado quando o destino é retirado da backstack, garantindo que o estado seja limpo automaticamente. Isso é diferente da detecção do descarte de elementos que podem ser compostos, que pode ocorrer por vários motivos, como abertura de uma nova tela devido a uma mudança de configuração etc.
- Integração com outras bibliotecas do Jetpack, como a Hilt.
Como os holders de estado são agrupáveis e os ViewModels e holders de estado simples têm responsabilidades diferentes, é possível que um elemento de composição da tela tenha tanto um ViewModel que dá acesso à lógica de negócios quanto um holder de estado que gerencia a lógica da IU e o estado dos elementos da IU. Como os ViewModels têm um ciclo de vida mais longo que o dos detentores de estado, eles poderão usar os ViewModels como uma dependência, se necessário.
O código a seguir mostra um ViewModel e um detentor de estado simples trabalhando juntos em uma ExampleScreen
:
class ExampleState(
val lazyListState: LazyListState,
private val resources: Resources,
private val expandedItems: List<Item> = emptyList()
) {
fun isExpandedItem(item: Item): Boolean = TODO()
/* ... */
}
@Composable
fun rememberExampleState(/* ... */): ExampleState { TODO() }
@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {
val uiState = viewModel.uiState
val exampleState = rememberExampleState()
LazyColumn(state = exampleState.lazyListState) {
items(uiState.dataToDisplayOnScreen) { item ->
if (exampleState.isExpandedItem(item)) {
/* ... */
}
/* ... */
}
}
}
Saiba mais
Para saber mais sobre o estado e Jetpack Compose, consulte os recursos abaixo.
Codelabs
Vídeos
- Um estado de espírito do Compose (vídeo em inglês)