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 do Room até uma variável em uma classe.
Todos os apps Android mostram 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ções 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 estado e elementos combináveis, 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 combinável. Esses argumentos são representações do
estado da interface. Sempre que um estado é atualizado, ocorre uma recomposição. Por isso,
itens como TextField
não são atualizados automaticamente como seriam em
visualizações imperativas baseadas em XML. Um elemento combinável precisa ser explicitamente informado sobre o novo estado
para que seja atualizado corretamente.
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
Se você executar esse código e tentar inserir texto, vai notar que nada acontece. Isso ocorre
porque o TextField
não se atualiza. Ele é atualizado quando o parâmetro
value
muda. Isso se deve
à maneira como a composição e a recomposição funcionam no
Compose.
Para saber mais sobre a composição inicial e a recomposição, consulte Trabalhando com o Compose.
Estado em elementos combináveis
As funções combináveis podem usar a
API remember
para armazenar um objeto na memória. Um valor calculado pela remember
é
armazenado durante
a composição inicial e retornado durante a recomposição.
A API remember
pode ser usada para armazenar tanto objetos mutáveis quanto 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
agenda a recomposição de todas as funções combináveis
que leem value
.
Há três maneiras de declarar um objeto MutableState
em um elemento combinável:
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 combinável 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 combináveis 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 quando 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.bodyMedium ) } 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 armazenamento personalizado.
Outros tipos de estado com suporte
O Compose não exige que você use MutableState<T>
para manter o estado. Ele
oferece suporte a outros tipos observáveis. Antes de ler outro tipo observável no
Compose, você precisa convertê-lo em State<T>
para que os elementos combináveis possam
ser recompostados automaticamente quando o estado mudar.
O Compose tem funções integradas para criar State<T>
com base em tipos observáveis comuns
usados em apps Android. Antes de usar essas integrações, adicione os
artefatos adequados, conforme descrito abaixo:
Flow
:collectAsStateWithLifecycle()
O
collectAsStateWithLifecycle()
coleta valores de umFlow
(link em inglês) considerando o ciclo de vida, permitindo que o app conserve recursos. Ele representa o valor emitido mais recentemente pelo ComposeState
. Use essa API como a maneira recomendada de coletar fluxos em apps Android.A seguinte dependência é necessária no arquivo
build.gradle
(precisa ser 2.6.0-beta01 ou mais recente):
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.5")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.5"
}
Flow
(link em inglês):collectAsState()
O
collectAsState
é semelhante aocollectAsStateWithLifecycle
porque também coleta valores de umFlow
e o transforma em umState
do Compose.Use o
collectAsState
para o código independente de plataforma em vez decollectAsStateWithLifecycle
, que é exclusivo para o Android.Outras dependências não são necessárias para
collectAsState
porque ele está disponível emcompose-runtime
.-
O
observeAsState()
começa a observar esteLiveData
e representa os valores dele com umState
.A dependência abaixo é necessária no arquivo
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.3"
}
-
subscribeAsState()
são funções de extensão que transformam os streams reativos do RxJava2, por exemplo,Single
,Observable
eCompletable
(links em inglês), em umState
do Compose.A dependência abaixo é necessária no arquivo
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.3"
}
-
subscribeAsState()
são funções de extensão que transformam os streams reativos do RxJava3, por exemplo,Single
,Observable
eCompletable
(links em inglês), em umState
do Compose.A dependência abaixo é necessária no arquivo
build.gradle
:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.7.3")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.3"
}
Com estado X sem estado
Um elemento combinável 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 têm
estado interno tendem a ser menos reutilizáveis e mais difíceis de testar.
Um elemento combinável sem estado é aquele que não tem estado algum. Uma maneira fácil de ficar sem estado é usar a elevação de estado.
Ao desenvolver elementos combináveis 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 que o autor da chamada possa transformar e remover o estado de um combinável. 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
adequados para o elemento combinável, defina-os usando lambdas.
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 combináveis com estado poderão modificar esse estado. Ele é totalmente interno.
- Compartilhável: o estado elevado pode ser compartilhado com vários elementos combináveis. Se você
quiser ler
name
em um combinável diferente, a elevação permitirá fazer isso. - Interceptável: os autores de chamada para elementos combináveis sem estado podem decidir ignorar ou modificar eventos antes de mudar o estado.
- Desacoplado:o estado dos elementos combináveis sem estado pode ser armazenado
em qualquer lugar. Por exemplo, agora é possível mover
name
para umViewModel
.
No exemplo, name
e onValueChange
são extraídos de
HelloContent
e movidos para cima na árvore até um elemento HelloScreen
combinável
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.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
Ao elevar o estado do HelloContent
, é mais fácil entender, reutilizar em situações diferentes e testar o
combinável. O HelloContent
está
dissociado do modo como o estado é armazenado. Isso significa que, se você modifica ou
substitui HelloScreen
, não precisa mudar a forma como 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 interface das partes do app que armazenam e mudam o estado.
Consulte a página Onde elevar o estado para saber mais.
Como restaurar o estado no Compose
A API rememberSaveable
se comporta de maneira semelhante a remember
porque
ela mantém o estado nas recomposições e também na recriação de atividades ou
processos usando o mecanismo de estado da instância salvo. Isso acontece, por exemplo,
quando a tela é girada.
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
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, @Parcelize
não for adequado, use mapSaver
para
definir sua própria regra de conversão de um objeto em um conjunto de valores que o
sistema pode 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 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")) } }
Detentores de estado no Compose
A elevação de estado simples pode ser gerenciada nas próprias funções combináveis. No entanto, caso a quantidade de estados a serem gerenciados aumente ou surja uma lógica para realizar em funções combináveis, é recomendável delegar as responsabilidades de lógica e estado a outras classes: detentores de estado.
Consulte a documentação sobre elevação de estado no Compose ou, de forma mais geral, a página Detentores de estado e estado da interface no guia de arquitetura para saber mais.
Reativar cálculos de recuperação quando as chaves mudarem
A API remember
é frequentemente usada com MutableState
:
var name by remember { mutableStateOf("") }
Aqui, o uso da função remember
faz com que o valor MutableState
sobreviva
a recomposições.
Em geral, remember
usa um parâmetro lambda calculation
. Quando remember
é executado pela primeira vez, ele invoca o lambda calculation
e armazena o resultado. Durante
a recomposição, remember
retorna o valor armazenado pela última vez.
Além do estado de armazenamento em cache, também é possível usar remember
para armazenar qualquer objeto ou
resultado de uma operação na composição que é cara para
inicializar ou calcular. Talvez você não queira repetir esse cálculo a cada recomposição.
Um exemplo é a criação deste objeto ShaderBrush
, que é uma operação
cara:
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
O remember
armazena o valor até sair da composição. No entanto, há uma
maneira de invalidar o valor armazenado em cache. A API remember
também usa um parâmetro key
ou
keys
. Se qualquer uma dessas chaves mudar, na próxima recomposição
da função, remember
vai invalidar o cache e executar
o bloco lambda de cálculo novamente. Esse mecanismo oferece controle sobre a vida útil de um
objeto na composição. O cálculo permanece válido até que as entradas
mudem, e não até que o valor lembrado saia da composição.
Os exemplos a seguir mostram como esse mecanismo funciona.
Neste snippet, um ShaderBrush
é criado e usado como a pintura em segundo plano
de um elemento combinável Box
. remember
armazena a instância ShaderBrush
porque a recriação dela é cara, conforme explicado anteriormente. remember
usa
avatarRes
como o parâmetro key1
, que é a imagem de plano de fundo selecionada. Se
avatarRes
muda, o pincel é recomposto com a nova imagem e se aplica de novo ao
Box
. Isso pode ocorrer quando o usuário seleciona outra imagem para ser o
segundo plano de um seletor.
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
No próximo snippet, o estado é elevado para uma classe detentora de estado simples
MyAppState
. Ele expõe uma função rememberMyAppState
para inicializar uma
instância da classe usando remember
. A exposição dessas funções para criar uma
instância que resiste a recomposições é um padrão comum no Compose. A
função rememberMyAppState
recebe windowSizeClass
, que serve como
o parâmetro key
para remember
. Se esse parâmetro mudar, o app vai precisar
recriar a classe de detentor de estado simples com o valor mais recente. Isso pode ocorrer se,
por exemplo, o usuário gira o dispositivo.
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
O Compose usa a implementação de equalização da classe para decidir se uma chave mudou e invalida o valor armazenado.
Armazenar o estado com chaves além da recomposição
A API rememberSaveable
é um wrapper em torno de remember
que pode armazenar
dados em um Bundle
. Essa API permite que o estado sobreviva não apenas
à recomposição, mas também à recriação de atividades e à interrupção do processo iniciada pelo sistema.
rememberSaveable
recebe parâmetros input
para a mesma finalidade que
remember
recebe keys
. O cache é invalidado quando qualquer uma das entradas
muda. Na próxima vez que a função for recomposta, o rememberSaveable
vai executar
o bloco lambda de cálculo de novo.
No exemplo a seguir, rememberSaveable
armazena userTypedQuery
até que
typedQuery
mude:
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
Saiba mais
Para saber mais sobre o estado e Jetpack Compose, consulte os recursos abaixo.
Exemplos
Codelabs
Vídeos
- Um estado de espírito do Compose (vídeo em inglês)
Blogs
- Gerenciamento de estado eficaz para
TextField
no Compose (link em inglês)
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Como arquitetar a interface do Compose
- Salvar o estado da interface no Compose
- Efeitos colaterais no Compose