Estado e Jetpack Compose

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 um Flow (link em inglês) considerando o ciclo de vida, permitindo que o app conserve recursos. Ele representa o valor emitido mais recentemente pelo State do Compose. 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.7")
}

Groovy

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
  • Flow (link em inglês): collectAsState()

    O collectAsState é semelhante ao collectAsStateWithLifecycle porque também coleta valores de um Flow e o transforma em um State do Compose.

    Use o collectAsState para o código independente de plataforma em vez de collectAsStateWithLifecycle, que é exclusivo para o Android.

    Outras dependências não são necessárias para collectAsState porque ele está disponível em compose-runtime.

  • LiveData: observeAsState()

    O observeAsState() começa a observar este LiveData e representa os valores dele com um State.

    A dependência abaixo é necessária no arquivo build.gradle:

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.7.5")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.7.5")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.7.5")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.7.5"
}

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 que T é 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 um ViewModel.

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 mantém o estado nas recomposições e na recriação de atividades ou processos usando o mecanismo de estado de 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

Blogs