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, é necessário convertê-lo em um State<T> para que os elementos combináveis possam ser recompostos 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 Compose State. 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):

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}
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:

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.7.8")
}
dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.7.8"
}
dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.7.8")
}
dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.7.8"
}
dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.7.8")
}
dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.7.8"
}

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 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. 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.

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

Jetnews is a sample news reading app, built with Jetpack Compose. The goal of the sample is to showcase the current UI capabilities of Compose. To try out this sample app, use the latest stable version of Android Studio. You can clone this repository

Jetchat is a sample chat app built with Jetpack Compose. To try out this sample app, use the latest stable version of Android Studio. You can clone this repository or import the project from Android Studio following the steps here. This sample

Learn how this app was designed and built in the design case study, architecture learning journey and modularization learning journey. This is the repository for the Now in Android app. It is a work in progress 🚧. Now in Android is a fully functional

Codelabs

Vídeos

Blogs