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 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 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.
Composição inicial:a criação de uma composição ao executar elementos combináveis pela primeira vez.
Recomposição:nova execução de elementos combináveis para atualizar a composição quando os dados mudam.
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 por remember é armazenado na composição
durante a composição inicial, e o valor armazenado é retornado durante
a recomposiçã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 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 é compatível com outros tipos observáveis. Antes de ler outro tipo observável no
Compose, você precisa convertê-lo em um State<T> para que os elementos combináveis possam
fazer a recomposição automaticamente quando o estado for modificado.
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 da seguinte forma:
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.10.0")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.10.0"
}
Flow(link em inglês):collectAsState()O
collectAsStateé semelhante aocollectAsStateWithLifecycleporque também coleta valores de umFlowe o transforma em umStatedo Compose.Use o
collectAsStatepara o código independente de plataforma em vez decollectAsStateWithLifecycle, que é exclusivo para o Android.Outras dependências não são necessárias para
collectAsStateporque ele está disponível emcompose-runtime.-
O
observeAsState()começa a observar esseLiveDatae representa os valores usandoState.A dependência abaixo é necessária no arquivo
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.12.0")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.12.0"
}
-
subscribeAsState()são funções de extensão que transformam os streams reativos do RxJava2, por exemplo,Single,ObservableeCompletable(links em inglês), em umStatedo Compose.A dependência abaixo é necessária no arquivo
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.12.0")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.12.0"
}
-
subscribeAsState()são funções de extensão que transformam os streams reativos do RxJava3, por exemplo,Single,ObservableeCompletable(links em inglês), em umStatedo Compose.A dependência abaixo é necessária no arquivo
build.gradle:
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.12.0")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.12.0"
}
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 comum de alcançar a ausência de estado é usando 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
apropriados para o elemento combinável, defina-os usando lambdas.
O estado elevado dessa maneira tem algumas propriedades importantes:
- Única fonte de 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
nameem 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.
- Dissociado:o estado dos elementos combináveis sem estado pode ser armazenado
em qualquer lugar. Por exemplo, agora é possível mover o
namepara 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.
- O estado precisa ser elevado pelo menos até o pai menos comum de todos os elementos combináveis que usam esse estado (leitura).
- O estado precisa ser elevado pelo menos até o nível mais alto de mudança possível (gravação).
- Se dois estados mudarem em resposta aos mesmos eventos, eles precisarão ser elevados juntos.
Você pode elevar um estado além do que essas regras exigem, mas se elevar abaixo do mínimo, isso vai dificultar ou impossibilitar seguir o fluxo de dados unidirecional.
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
mantém o estado entre recomposições e também entre recriações de atividades ou processos
usando o mecanismo de estado da instância salvo. Por exemplo, isso acontece
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.
Em outros materiais, os detentores de estado também são chamados de objetos de estado elevado.
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
TextFieldno 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