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é 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çõ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 IU. 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
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 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. No caso do ExpandingCard, sempre que expanded muda, o ExpandingCard é recomposto.

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

Outros tipos de estado com suporte

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 State<T> para que o Jetpack Compose possa fazer automaticamente a recomposição 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, conforme descrito abaixo:

  • Flow (link em inglês): collectAsStateWithLifecycle()

    O collectAsStateWithLifecycle() coleta valores de um Flow (link em inglês) considerando o ciclo de vida, permitindo que o app não use recursos desnecessários. Ele representa o valor emitido mais recentemente pelo Compose State. Use essa API como a maneira recomendada de coletar fluxos em apps Android.

    A dependência abaixo é necessária no arquivo build.gradle. O uso da versão Alfa mais recente, 2.6.0-alpha03, para aproveitar a API collectAsStateWithLifecycle é temporário:

Kotlin

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03")
}

Groovy

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03"
}
  • 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.3.2")
}

Groovy

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

Kotlin

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

Groovy

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

Kotlin

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

Groovy

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

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 apropriados para o elemento combinável, 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 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.
  • Dissociado: o estado do ExpandingCard sem estado pode ser armazenado em qualquer lugar. Por exemplo, agora é possível mover o 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.h5
        )
        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 IU das partes do app que armazenam e mudam o estado.

Como restaurar o estado no Compose

Use 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, @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"))
    }
}

Como gerenciar o 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 elevação de estado na documentação do Compose ou então, de forma mais geral, as orientações de arquitetura sobre detentores de estado.

Saiba mais

Para saber mais sobre o estado e Jetpack Compose, consulte os recursos abaixo.

Exemplos

Codelabs

Vídeos

Blogs