Como usar o estado no Jetpack Compose

1. Introdução

Neste codelab, você vai aprender sobre o estado e como ele pode ser usado e manipulado pelo Jetpack Compose.

Antes de começar, é útil definir exatamente o que é o estado. Na essência, o estado de um aplicativo é qualquer valor que possa mudar com o tempo. Essa é uma definição muito ampla e abrange tudo, desde um banco de dados do Room até uma variável de classe.

Todos os apps Android exibem o estado ao usuário. Alguns exemplos de estado em apps Android:

  1. Um snackbar que mostra quando não é possível estabelecer uma conexão de rede
  2. Uma postagem de blog e comentários associados
  3. Animações de ondulação em botões quando um usuário clica neles
  4. Adesivos que podem ser mostrados em cima de uma imagem

Neste codelab, você vai aprender a usar e avaliar o estado ao usar o Jetpack Compose. Para fazer isso, criaremos um aplicativo "TODO" de lista de tarefas. Ao final deste codelab, você vai ter criado uma IU com estado que exibe uma lista de tarefas interativa e editável.

b5c4dc05d1e54d5a.png

Na próxima seção, você vai aprender sobre o fluxo de dados unidirecional, um padrão de design básico para entender como exibir e gerenciar o estado ao usar o Compose.

O que você vai aprender

  • O que é fluxo de dados unidirecional.
  • Como considerar o estado e os eventos em uma IU.
  • Como usar o ViewModel e o LiveData do componente de arquitetura no Compose para gerenciar o estado.
  • Como o Compose usa o estado para mostrar uma tela.
  • Quando mover o estado para um autor de chamada.
  • Como usar o estado interno no Compose.
  • Como usar o State<T> para integrar o estado ao Compose.

O que é necessário

O que você vai criar

  • O app TODO de lista de tarefas interativa usando o fluxo de dados unidirecional no Compose

2. Etapas da configuração

Para fazer o download do app de exemplo, você pode:

Ou clonar o repositório do GitHub pela linha de comando usando o comando abaixo:

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/StateCodelab

Você pode executar qualquer um dos módulos no Android Studio a qualquer momento mudando a configuração de execução na barra de ferramentas.

b059413b0cf9113a.png

Abrir o projeto no Android Studio

  1. Na janela "Welcome to Android Studio", selecione c01826594f360d94.png Open an Existing Project.
  2. Selecione a pasta [Download Location]/StateCodelab. Dica: selecione o diretório StateCodelab que contém build.gradle.
  3. Depois que o Android Studio importar o projeto, teste se você pode executar os módulos start e finished.

Conhecer o código inicial

O código inicial contém quatro pacotes:

  • examples: exemplos de atividades para explorar os conceitos do fluxo de dados unidirecional. Não é necessário editar este pacote.
  • ui: contém temas gerados automaticamente pelo Android Studio ao iniciar um novo projeto no Compose. Não é necessário editar este pacote.
  • util: contém o código auxiliar do projeto. Não é necessário editar este pacote.
  • todo: o pacote que contém o código da tela da lista de tarefas que estamos criando. Você vai fazer modificações neste pacote.

Este codelab se concentra nos arquivos do pacote todo. No módulo start, há vários arquivos que você precisa conhecer.

Arquivos fornecidos no pacote todo

  • Data.kt: estruturas de dados usadas para representar um TodoItem.
  • TodoComponents.kt: elementos que podem ser compostos e reutilizados que vão ser usados para criar a tela do app. Não é necessário editar este arquivo.

Arquivos que você vai editar no pacote todo

  • TodoActivity.kt: atividade do Android que vai usar o Compose para mostrar uma tela do app "Todo" após o codelab.
  • TodoViewModel.kt: um ViewModel que você vai integrar ao Compose para criar a tela do Todo. Você vai o conectar ao Compose e ampliá-lo para adicionar mais recursos durante este codelab.
  • TodoScreen.kt: implementação do Compose em uma tela do app "Todo" que você vai criar durante este codelab.

3. Noções básicas sobre o fluxo de dados unidirecional

Loop de atualização da IU

Antes de começar a usar nosso app "TODO", vamos conhecer os conceitos do fluxo de dados unidirecional usando o sistema de visualização do Android.

O que faz com que o estado seja atualizado? Na introdução, falamos sobre o estado como qualquer valor que muda ao longo do tempo. Isso faz parte apenas da história do estado em um app Android.

Nos apps Android, o estado é atualizado em resposta aos eventos. Eventos são entradas geradas fora do nosso app, como o toque do usuário em um botão que chama um OnClickListener, um EditText que chama um afterTextChanged ou um acelerômetro que envia um novo valor.

Todos os apps Android têm um loop de atualização de IU principal que funciona assim:

f415ca9336d83142.png

  • Evento: um evento é gerado pelo usuário ou por outra parte do programa.
  • Estado de atualização: um manipulador de eventos muda o estado usado pela IU.
  • Estado de exibição: a IU é atualizada para exibir o novo estado.

O gerenciamento de estado no Compose tem como objetivo entender como o estado e os eventos interagem entre si.

Estado não estruturado

Antes de começar a usar o Compose, vamos conhecer os eventos e o estado no sistema de visualização do Android. Como um estado "Hello, World", vamos criar uma Activity "Hello World" que permite ao usuário inserir o nome dele.

879ed27ccab2eed3.gif

Uma maneira de escrever isso é fazer com que o callback do evento defina diretamente o estado na TextView e o código, usando a ViewBinding, pode ser assim:

HelloCodelabActivity**.kt**

class HelloCodelabActivity : AppCompatActivity() {

   private lateinit var binding: ActivityHelloCodelabBinding
   var name = ""

   override fun onCreate(savedInstanceState: Bundle?) {
       /* ... */
       binding.textInput.doAfterTextChanged {text ->
           name = text.toString()
           updateHello()
       }
   }

   private fun updateHello() {
       binding.helloText.text = "Hello, $name"
   }
}

Esse código cumpre o papel dele. Para um exemplo pequeno como esse, tudo bem. No entanto, isso se torna difícil de gerenciar à medida que a IU cresce.

À medida que você adicionar mais eventos e estados a uma atividade criada assim, podem surgir vários problemas:

  1. Testes: como o estado da IU está interligado às Views, pode ser difícil testar esse código.
  2. Atualizações de estado parcial: quando a tela tem muitos outros eventos, é fácil esquecer que uma parte do estado vai ser atualizada em resposta a um evento. Como resultado, o usuário pode ver uma IU inconsistente ou incorreta.
  3. Atualizações parciais da IU: como estamos atualizando manualmente a IU após cada mudança de estado, às vezes é fácil esquecer isso. Como resultado, o usuário pode ver dados desatualizados na IU que são atualizados aleatoriamente.
  4. Complexidade do código: é difícil extrair parte da lógica ao codificar nesse padrão. Como resultado, o código tende a se tornar difícil de ler e entender.

Como usar o fluxo de dados unidirecional

Para ajudar a corrigir esses problemas com o estado não estruturado, lançamos os Componentes da arquitetura do Android, que contêm ViewModel e LiveData.

Um ViewModel permite extrair o estado da IU e definir eventos que a IU pode chamar para atualizar esse estado. Vejamos a mesma atividade escrita usando um ViewModel.

8a331b9c1b392bef.png

HelloCodelabActivity.kt

class HelloCodelabViewModel: ViewModel() {

   // LiveData holds state which is observed by the UI
   // (state flows down from ViewModel)
   private val _name = MutableLiveData("")
   val name: LiveData<String> = _name

   // onNameChanged is an event we're defining that the UI can invoke
   // (events flow up from UI)
   fun onNameChanged(newName: String) {
       _name.value = newName
   }
}

class HelloCodeLabActivityWithViewModel : AppCompatActivity() {
   private val helloViewModel by viewModels<HelloCodelabViewModel>()

   override fun onCreate(savedInstanceState: Bundle?) {
       /* ... */

       binding.textInput.doAfterTextChanged {
           helloViewModel.onNameChanged(it.toString())
       }

       helloViewModel.name.observe(this) { name ->
           binding.helloText.text = "Hello, $name"
       }
   }
}

Neste exemplo, movemos o estado da Activity para um ViewModel. Em um ViewModel, o estado é representado por LiveData. Um objeto LiveData é um detentor de estado observável, o que significa que ele permite que qualquer pessoa observe mudanças no estado. Em seguida, usamos o método observe para atualizar a IU sempre que o estado mudar.

O ViewModel também expõe um evento: onNameChanged. Esse evento é chamado pela IU em resposta a eventos do usuário, como o que acontece aqui sempre que o texto do EditText muda.

Voltando ao loop de atualização da IU que abordamos anteriormente, podemos ver como esse ViewModel se ajusta a eventos e estados.

  • Evento: onNameChanged é chamado pela IU quando a entrada de texto é modificada.
  • Estado de atualização: onNameChanged faz o processamento e define o estado de _name.
  • Estado de exibição: os observadores de name são chamados, o que notifica a IU das mudanças de estado.

Ao estruturar o código dessa maneira, podemos pensar em eventos que "fluem" para ViewModel. Em seguida, em resposta a eventos, o ViewModel vai fazer algum processamento e possivelmente atualizar o estado. Quando o estado é atualizado, ele "flui" para a Activity.

O estado flui do ViewModel para a atividade, enquanto os eventos fluem da atividade para o ViewModel.

Esse padrão é chamado de fluxo de dados unidirecional. O fluxo de dados unidirecional é um design em que os estados fluem para baixo e os eventos para cima. Ao estruturar o código dessa maneira, podemos oferecer algumas vantagens:

  • Capacidade de teste: ao desacoplar o estado da IU que o exibe, fica mais fácil testar o ViewModel e a atividade.
  • Encapsulamento de estado: como o estado pode ser atualizado em um lugar (o ViewModel), é menos provável que você introduza um bug de atualização de estado parcial à medida que a IU cresce.
  • Consistência da IU: todas as atualizações de estado são refletidas imediatamente na IU pelo uso de detentores de estado observáveis.

Assim, embora essa abordagem adicione um pouco mais de código, ela tende a ser mais fácil e confiável para processar estados e eventos complexos usando o fluxo de dados unidirecional.

Na próxima seção, vamos ver como usar o fluxo de dados unidirecional com o Compose.

4. Compose e ViewModels

Na última seção, abordamos o fluxo de dados unidirecional no sistema de visualizações do Android usando ViewModel e LiveData. Agora vamos aprender a usar o fluxo de dados unidirecional no Compose com ViewModels.

No final desta seção, você vai ter criado esta tela:

7998ef0a441d4b3.png

Conhecer os elementos que podem ser compostos da TodoScreen

O código transferido por download contém vários elementos que podem ser compostos que vão ser usados e editados ao longo deste codelab.

Abra TodoScreen.kt e veja o elemento TodoScreen que pode ser composto:

TodoScreen.kt

@Composable
fun TodoScreen(
   items: List<TodoItem>,
   onAddItem: (TodoItem) -> Unit,
   onRemoveItem: (TodoItem) -> Unit
) {
   /* ... */
}

Para ver o que esse elemento que pode ser composto exibe, use o painel de visualização no Android Studio clicando no ícone de divisão no canto direito de cima 52dd4dd99bae0aaf.png.

4cedcddc3df7c5d6.png

Esse elemento que pode ser composto exibe uma lista de tarefas editável, mas não tem nenhum estado próprio. Lembre-se que o estado é qualquer valor que pode mudar, mas nenhum dos argumentos da TodoScreen pode ser modificado.

  • items: uma lista imutável de itens a serem mostrados na tela.
  • onAddItem: um evento para quando o usuário solicita a adição de um item.
  • onRemoveItem: um evento para quando o usuário solicita a remoção de um item.

Na verdade, esse elemento que pode ser composto é sem estado. Ele exibe somente a lista de itens que foi transmitida e não tem maneiras de editar diretamente a lista. Em vez disso, ele recebe dois eventos onRemoveItem e onAddItem que podem solicitar mudanças.

Isso levanta a questão: se for sem estado, como ele pode exibir uma lista editável? Isso é feito com uma técnica chamada elevação de estado. A elevação de estado é o padrão para mover um estado para cima e tornar um componente em sem estado. Componentes sem estado são mais fáceis de testar, tendem a ter menos bugs e abrem mais oportunidades para reutilização.

Na verdade, a combinação desses parâmetros funciona para permitir que o autor da chamada eleve o estado desse elemento que pode ser composto. Para ver como isso funciona, veja o loop de atualização da IU desse elemento.

  • Evento: quando o usuário solicita a adição ou remoção de um item, o TodoScreen chama onAddItem ou onRemoveItem.
  • Estado de atualização: o autor da chamada de TodoScreen pode responder a esses eventos atualizando o estado.
  • Estado de exibição: quando o estado for atualizado, a TodoScreen vai ser chamada novamente com os novos items e vai poder os exibir na tela.

O autor da chamada é responsável por descobrir onde e como manter esse estado. No entanto, ele pode armazenar items na memória ou os ler de um banco de dados do Room. A TodoScreen é completamente separada da forma como o estado é gerenciado.

Definir a composição da TodoActivityScreen

Abra TodoViewModel.kt e encontre um ViewModel que defina uma variável de estado e dois eventos.

TodoViewModel.kt

class TodoViewModel : ViewModel() {

   // state: todoItems
   private var _todoItems = MutableLiveData(listOf<TodoItem>())
   val todoItems: LiveData<List<TodoItem>> = _todoItems

   // event: addItem
   fun addItem(item: TodoItem) {
        /* ... */
   }

   // event: removeItem
   fun removeItem(item: TodoItem) {
        /* ... */
   }
}

Queremos usar esse ViewModel para elevar o estado da TodoScreen. Quando terminarmos, teremos criado um design de fluxo de dados unidirecional que vai ter esta aparência:

f555d7b9be40144c.png

Para começar a integrar a TodoScreen na TodoActivity, abra o arquivo TodoActivity.kt e defina uma nova função @Composable TodoActivityScreen(todoViewModel: TodoViewModel) e a chame em setContent no método onCreate.

No restante desta seção, vamos criar a TodoActivityScreen um passo por vez. Para começar, chame a TodoScreen com um estado falso e eventos como este:

TodoActivity.kt

import androidx.compose.runtime.Composable

class TodoActivity : AppCompatActivity() {

   private val todoViewModel by viewModels<TodoViewModel>()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           StateCodelabTheme {
               Surface {
                   TodoActivityScreen(todoViewModel)
               }
           }
       }
   }
}

@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   val items = listOf<TodoItem>() // in the next steps we'll complete this
   TodoScreen(
       items = items,
       onAddItem = { }, // in the next steps we'll complete this
       onRemoveItem = { } // in the next steps we'll complete this
   )
}

Esse elemento que pode ser composto vai ser uma ponte entre o estado armazenado no ViewModel e o elemento TodoScreen que já está definido no projeto. Você pode mudar a TodoScreen para usar o ViewModel diretamente, mas a TodoScreen seria um pouco menos reutilizável. Ao dar preferência a parâmetros mais simples, como List<TodoItem>, a TodoScreen não está acoplada ao local específico em que o estado é elevado.

Se você executar o app agora, vai ver que ele exibe um botão, mas clicar nele não faz nada. Isso ocorre porque ainda não conectamos nosso ViewModel à TodoScreen.

a195c5b4d2a5ea0f.png

Fluxo de eventos para cima

Agora que temos todos os componentes necessários, um ViewModel, uma TodoActivityScreen que pode ser composta como ponte e uma TodoScreen, vamos conectar tudo para exibir uma lista dinâmica usando um fluxo de dados unidirecional.

Na TodoActivityScreen, transmita addItem e removeItem do ViewModel.

TodoActivity.kt

@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   val items = listOf<TodoItem>()
   TodoScreen(
       items = items,
       onAddItem = { todoViewModel.addItem(it) },
       onRemoveItem = { todoViewModel.removeItem(it) }
   )
}

Quando a TodoScreen chama onAddItem ou onRemoveItem, podemos transmitir a chamada para o evento correto no ViewModel.

Transmitir o estado para baixo

Transferimos os eventos do nosso fluxo de dados unidirecional e agora precisamos transmitir o estado.

Edite a TodoActivityScreen para observar os todoItems do LiveData usando observeAsState:

TodoActivity.kt

import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState

@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   val items: List<TodoItem> by todoViewModel.todoItems.observeAsState(listOf())
   TodoScreen(
       items = items,
       onAddItem = { todoViewModel.addItem(it) },
       onRemoveItem = { todoViewModel.removeItem(it) }
   )
}

Essa linha vai observar o LiveData e nos permite usar o valor atual diretamente como uma List<TodoItem>.

Há muita coisa nessa linha, então vamos separar:

  • val items: List<TodoItem> declara uma variável items do tipo List<TodoItem>.
  • todoViewModel.todoItems é um LiveData<List<TodoItem> do ViewModel.
  • O .observeAsState observa um LiveData<T> e o converte em um objeto State<T> para que o Compose possa reagir às mudanças de valor.
  • listOf() é um valor inicial para evitar possíveis resultados null antes do LiveData ser inicializado. Se ele não for transmitido, os items seriam List<TodoItem>?, que é anulável.
  • by é a sintaxe de delegação de propriedade em Kotlin, que permite separar automaticamente State<List<TodoItem>> de observeAsState em uma List<TodoItem> comum.

Executar o app novamente

Execute o app novamente para ver uma lista que é atualizada dinamicamente. Clique no botão na parte de baixo para adicionar novos itens. Clique em um deles para remover.

7998ef0a441d4b3.png

Nesta seção, vimos como criar um design de fluxo de dados unidirecional no Compose usando ViewModels. Também vimos como usar um elemento que pode ser composto sem estado para exibir uma IU com estado usando uma técnica chamada elevação de estado. Além disso, continuamos estudando as IUs dinâmicas em termos de estado e eventos.

Na próxima seção, você vai aprender a adicionar memória a funções que podem ser compostas.

5. Memória no Compose

Agora que vimos como usar o Compose com ViewModels para criar um fluxo de dados unidirecional, vamos ver como o Compose pode interagir com o estado internamente.

Na última seção, você viu como o Compose atualiza a tela chamando novamente os elementos que podem ser compostos. Esse processo é conhecido como recomposição. Conseguimos exibir uma lista dinâmica chamando TodoScreen novamente.

Nesta e na próxima seção, vamos ver como adicionar estado aos elementos que podem ser compostos.

Nesta seção, vamos ver como adicionar memória a uma função de composição, que é um elemento básico necessário para adicionar o estado ao Compose na próxima seção.

Design desordenado

Simulação do designer

40a46273d161497a.png

Nesta seção, um novo designer da sua equipe fez uma simulação seguindo a tendência de design mais recente: design desordenado. O princípio fundamental do design desordenado é fazer um bom design e adicionar mudanças aparentemente aleatórias para que seja "interessante".

Neste design, cada ícone é colorido para um valor alfa aleatório entre 0,3 e 0,9.

Como adicionar elementos aleatórios a um elemento que pode ser composto

Para começar, abra o arquivo TodoScreen.kt e encontre o elemento TodoRow que pode ser composto. Esse elemento que pode ser composto descreve uma única linha na lista do app de lista de afazeres.

Defina um novo val iconAlpha com um valor de randomTint(). Conforme solicitado pelo nosso designer, esse é um ponto flutuante entre 0,3 e 0,9. Em seguida, defina a tonalidade do ícone.

TodoScreen.kt

import androidx.compose.material.LocalContentColor

@Composable
fun TodoRow(todo: TodoItem, onItemClicked: (TodoItem) -> Unit, modifier: Modifier = Modifier) {
   Row(
       modifier = modifier
           .clickable { onItemClicked(todo) }
           .padding(horizontal = 16.dp, vertical = 8.dp),
       horizontalArrangement = Arrangement.SpaceBetween
   ) {
       Text(todo.task)
       val iconAlpha = randomTint()
       Icon(
           imageVector = todo.icon.imageVector,
           tint = LocalContentColor.current.copy(alpha = iconAlpha),
           contentDescription = stringResource(id = todo.icon.contentDescription)
       )
   }
}

Se você conferir a visualização novamente, vai ver que o ícone agora tem uma tonalidade aleatória.

cdb483885e713651.png

Conhecer a recomposição

Execute o app novamente para testar o novo design desordenado. Você vai perceber imediatamente que as tonalidades mudam o tempo todo. Seu designer disse que exageramos um pouco no modo aleatório.

App com ícones que mudam de tonalidade quando a lista muda

2e53e9411aeee11e.gif

O que está acontecendo aqui? Perceba que o processo de recomposição chama randomTint para cada linha na tela novamente sempre que a lista muda.

A recomposição é o processo de chamar funções que podem ser compostas novamente com novas entradas para atualizar a árvore do Compose. Nesse caso, quando a TodoScreen for chamada novamente com uma nova lista, a LazyColumn vai fazer a recomposição de todos os filhos na tela. Isso vai chamar a TodoRow novamente, gerando uma nova tonalidade aleatória.

O Compose gera uma árvore, mas é um pouco diferente da árvore de IU que você talvez conheça do sistema de visualização do Android. Em vez de uma árvore de widgets de IU, o Compose gera uma árvore de elementos que podem ser compostos. Podemos visualizar a TodoScreen desta forma:

Árvore da TodoScreen

6f5faa4342c63d88.png

Quando o Compose executa a composição na primeira vez, ele cria uma árvore de cada função que foi chamada. Em seguida, durante a recomposição, ele atualiza a árvore com os novos elementos que podem ser compostos chamados.

O ícone é atualizado sempre que a TodoRow faz a recomposição porque TodoRow tem um efeito colateral oculto. Um efeito colateral é qualquer mudança visível fora da execução de uma função de composição.

A chamada para Random.nextFloat() atualiza a variável aleatória interna usada em um gerador de números pseudoaleatórios. É assim que Random retorna um valor diferente sempre que você pede um número aleatório.

Introdução de memória em funções de composição

Não queremos que a tonalidade mude sempre que a TodoRow for recomposta. Para isso, precisamos de um lugar para lembrar a tonalidade que usamos na última composição. O Compose permite armazenar valores na árvore de composição. Assim, podemos atualizar a TodoRow para armazenar o iconAlpha na árvore de composição.

Edite a TodoRow e coloque a chamada de randomTint em remember desta maneira:

TodoScreen.kt

val iconAlpha: Float = remember(todo.id) { randomTint() }
Icon(
   imageVector = todo.icon.imageVector,
   tint = LocalContentColor.current.copy(alpha = iconAlpha),
   contentDescription = stringResource(id = todo.icon.contentDescription)
)

Observando a nova árvore de composição do Compose para TodoRow, é possível ver que iconAlpha foi adicionado:

Árvore da TodoRow usando "remember"

O diagrama mostra o iconAlpha como um novo filho da TodoRow na árvore de composição.

Se você executar o app novamente, vai ver que a tonalidade não é atualizada sempre que a lista mudar. Em vez disso, quando a recomposição acontece, o valor anterior armazenado por remember é retornado.

Se você analisar a chamada de "remember", vai ver que estamos transmitindo todo.id como o argumento key.

remember(todo.id) { randomTint() }

Uma chamada de "remember" tem duas partes:

  1. key arguments: a "chave" que esse remetente usa é a parte que é transmitida entre parênteses. Aqui, transmitimos todo.id como a chave.
  2. calculation: uma lambda que calcula um novo valor para ser lembrado, transmitido em uma lambda final. Estamos calculando um valor aleatório com randomTint().

Ao fazer a composição pela primeira vez, "remember" sempre chama randomTint e lembra o resultado para a próxima recomposição. Ele também monitora o todo.id que foi transmitido. Durante a recomposição, ele vai pular a chamada de randomTint e retornar o valor memorizado, a menos que um novo todo.id seja transmitido à TodoRow.

A recomposição de um elemento que pode ser composto precisa ser idempotente. Ao colocar a chamada em randomTint com remember, a chamada para um valor aleatório vai ser ignorada na recomposição, a menos que o item do app "Todo" mude. Como resultado, a TodoRow não tem efeitos colaterais e sempre produz o mesmo resultado quando for recomposta com a mesma entrada, além de ser idempotente.

Como tornar os valores memorizados controláveis

Se você executar o app agora, vai ver que ele está exibindo uma tonalidade aleatória em cada ícone. O designer está satisfeito com o fato de que o app está seguindo os princípios do design desordenado e o aprova para envio.

No entanto, há uma pequena mudança no código que precisa ser feita antes da verificação. No momento, não há como o autor da chamada de TodoRow especificar a tonalidade. Há muitos motivos para que o autor possa querer fazer a chamada. Por exemplo, o vice-presidente do produto pode perceber essa tela e exigir um hotfix para remover o desordenamento antes de enviar o app.

Para que o autor da chamada controle esse valor, basta mover a chamada de recall para um argumento padrão de um novo parâmetro iconAlpha.

@Composable
fun TodoRow(
   todo: TodoItem,
   onItemClicked: (TodoItem) -> Unit,
   modifier: Modifier = Modifier,
   iconAlpha: Float = remember(todo.id) { randomTint() }
) {
   Row(
       modifier = modifier
           .clickable { onItemClicked(todo) }
           .padding(horizontal = 16.dp)
           .padding(vertical = 8.dp),
       horizontalArrangement = Arrangement.SpaceBetween
   ) {
       Text(todo.task)
       Icon(
            imageVector = todo.icon.imageVector,
            tint = LocalContentColor.current.copy(alpha = iconAlpha),
            contentDescription = stringResource(id = todo.icon.contentDescription)
        )
   }
}

Agora, o autor da chamada recebe o mesmo comportamento por padrão: a TodoRow calcula randomTint. No entanto, eles podem especificar qualquer valor alfa. Ao permitir que o autor da chamada controle a alphaTint, esse elemento que pode ser composto se torna mais reutilizável. Em outra tela, um designer pode querer exibir todos os ícones com o valor alfa 0,7.

Também há um bug muito sutil com o uso de remember. Adicione linhas de tarefas suficientes para rolar algumas telas clicando em "Add random todo" (adicionar tarefa aleatória) várias vezes e rolando. Ao rolar a tela, você vai notar que os ícones mudam sempre que eles voltam para a tela.

Nas próximas seções, vamos ver o estado e a elevação de estado, que fornece as ferramentas necessárias para corrigir bugs como esses.

6. Estado no Compose

Na última seção, aprendemos como as funções que podem ser compostas têm memória. Agora vamos explorar como usar essa memória para adicionar estado a uma função que pode ser composta.

Entrada do app "Todo" (estado: expandida) 721446d6a55fcaba.png

Entrada do app "Todo" (estado: recolhida) 6f46071227df3625.png

Nosso designer passou do design desordenado e agora está no pós-Material. O novo design para entrada de tarefas ocupa o mesmo espaço que um cabeçalho recolhível, além de ter dois estados principais: expandido e recolhido. A versão expandida vai ser exibida quando o texto não estiver vazio.

Para criar isso, primeiro criamos o texto e o botão. Em seguida, vamos ver como adicionar os ícones de ocultação automática.

A edição de texto em uma IU é com estado. O usuário atualiza o texto exibido no momento sempre que digita um caractere ou mesmo quando muda a seleção. No sistema de visualização do Android, esse estado é interno ao EditText e exposto por listeners onTextChanged. No entanto, como o Compose foi criado para um fluxo de dados unidirecional, isso não seria adequado.

No Compose, TextField é um elemento que pode ser composto sem estado. Assim como a TodoScreen que exibe uma lista variável de tarefas, um TextField exibe apenas o que você quiser e emite eventos quando o usuário digita.

Criar um elemento TextField que pode ser composto com estado

Para começar a explorar o estado no Compose, faremos um componente com estado para exibir um TextField editável.

Para começar, abra TodoScreen.kt e adicione a função abaixo

TodoScreen.kt

import androidx.compose.runtime.mutableStateOf

@Composable
fun TodoInputTextField(modifier: Modifier) {
   val (text, setText) = remember { mutableStateOf("") }
   TodoInputText(text, setText, modifier)
}

Essa função usa remember para adicionar memória a si mesma. Em seguida, ela armazena um mutableStateOf para criar uma MutableState<String>, que é um tipo integrado do Compose que fornece um detentor de estado observável.

Como transmitiremos imediatamente um valor e um evento setter ao TodoInputText, vamos desestruturar o objeto MutableState em um getter e um setter.

Pronto. Criamos um estado interno em TodoInputTextField.

Para ver isso em ação, defina outra TodoItemInput que pode ser composta que mostre o TodoInputTextField e um Button.

TodoScreen.kt

import androidx.compose.ui.Alignment

@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
   // onItemComplete is an event will fire when an item is completed by the user
   Column {
       Row(Modifier
           .padding(horizontal = 16.dp)
           .padding(top = 16.dp)
       ) {
           TodoInputTextField(Modifier
               .weight(1f)
               .padding(end = 8.dp)
           )
           TodoEditButton(
               onClick = { /* todo */ },
               text = "Add",
               modifier = Modifier.align(Alignment.CenterVertically)
           )
       }
   }
}

TodoItemInput tem apenas um parâmetro, um evento onItemComplete. Quando o usuário concluir um TodoItem, o evento vai ser acionado. Esse padrão de transmitir uma lambda é a principal maneira de definir eventos personalizados no Compose.

Além disso, atualize o elemento TodoScreen que pode ser composto para chamar a TodoItemInput no TodoItemInputBackground em segundo plano, que já está definido no projeto:

TodoScreen.kt

@Composable
fun TodoScreen(
   items: List<TodoItem>,
   onAddItem: (TodoItem) -> Unit,
   onRemoveItem: (TodoItem) -> Unit
) {
   Column {
       // add TodoItemInputBackground and TodoItem at the top of TodoScreen
       TodoItemInputBackground(elevate = true, modifier = Modifier.fillMaxWidth()) {
           TodoItemInput(onItemComplete = onAddItem)
       }
...

Testar TodoItemInput

Como acabamos de definir uma IU que pode ser composta para o arquivo, é recomendável adicionar uma @Preview a ele. Com isso, poderemos analisar esse elemento que pode ser composto de forma isolada, além de permitir que os leitores desse arquivo o visualizem rapidamente.

No arquivo TodoScreen.kt, adicione uma nova função de visualização à parte de baixo:

TodoScreen.kt

@Preview
@Composable
fun PreviewTodoItemInput() = TodoItemInput(onItemComplete = { })

Agora você pode executar essa função em uma visualização interativa ou em um emulador para depurar a função isoladamente.

Depois, você vai ver um campo de texto editável que permite ao usuário editar o texto. Sempre que um caractere for digitado, o estado é atualizado e isso aciona a recomposição atualizando o TextField exibido ao usuário.

Mostrando PreviewTodoItemInput com o estado interativo em execução.

Clicar no botão e adicionar um item

Agora, queremos fazer com que o botão "Add" adicione um TodoItem. Para fazer isso, vamos precisar acessar o text do TodoInputTextField.

Se você analisar a parte da árvore de composição da TodoItemInput, vai ver que estamos armazenando o estado do texto no TodoInputTextField.

Árvore de composição de TodoItemInput (elementos integrados que podem ser compostos ocultos)

Árvore: TodoItemInput com filhos TodoInputTextField e TodoEditButton.  O texto do estado é um filho de TodoInputTextField.

Essa estrutura não nos permite conectar o método onClick porque onClick precisa acessar o valor atual do text. O que queremos fazer é expor o estado do text para a TodoItemInput e usar o fluxo de dados unidirecional ao mesmo tempo.

O fluxo de dados unidirecional se aplica à arquitetura de alto nível e ao design de um único elemento que pode ser composto ao usar o Jetpack Compose. Aqui, queremos que isso faça com que os eventos sempre fluam para cima e o estado para baixo.

Isso significa que queremos que o estado flua para baixo de TodoItemInput e que os eventos fluam para cima.

Diagrama de fluxo de dados unidirecional para TodoItemInput

Diagrama: TodoItemInput na parte de cima: estado flui para TodoInputTextField. Os eventos fluem de TodoInputTextField para TodoItemInput.

Para fazer isso, precisamos mover o estado do elemento filho que pode ser composto, TodoInputTextField, para o TodoItemInput pai.

Árvore de composição de TodoItemInput com elevação de estado (elementos integrados que podem ser compostos ocultos)

e2ccddf8af39d228.png

Esse padrão é chamado de elevação de estado. Vamos "elevar" o estado de um elemento que pode ser composto para o tornar sem estado. A elevação de estado é o padrão principal para criar designs de fluxo de dados unidirecionais no Compose.

Para começar a elevar um estado, é possível refatorar qualquer estado interno T de um elemento que pode ser composto para um par de parâmetros (value: T, onValueChange: (T) -> Unit).

Edite TodoInputTextField para elevar o estado adicionando parâmetros (value, onValueChange):

TodoScreen.kt

// TodoInputTextField with hoisted state

@Composable
fun TodoInputTextField(text: String, onTextChange: (String) -> Unit, modifier: Modifier) {
   TodoInputText(text, onTextChange, modifier)
}

Esse código adiciona um parâmetro value e onValueChange a TodoInputTextField. O parâmetro de valor é text e o parâmetro onValueChange é onTextChange.

Em seguida, como o estado agora está elevado, removemos o estado memorizado do TodoInputTextField.

O estado elevado dessa maneira tem algumas propriedades importantes:

  • Fonte única da verdade: ao mover o estado em vez de duplicar, garantimos que existe apenas uma fonte de verdade para o texto. Isso ajuda a evitar bugs.
  • Encapsulado: somente a TodoItemInput vai poder modificar o estado, enquanto outros componentes poderão enviar eventos para a TodoItemInput. Ao elevar dessa forma, somente um elemento que pode ser composto é com estado, mesmo que vários elementos usem o estado.
  • Compartilhável: o estado elevado pode ser compartilhado como um valor imutável com vários elementos que podem ser compostos. Aqui, usaremos o estado em TodoInputTextField e TodoEditButton.
  • Interceptável: TodoItemInput pode decidir ignorar ou modificar eventos antes de mudar o estado. Por exemplo, TodoItemInput pode formatar códigos :emoji-codes: para emojis conforme o usuário digita.
  • Desacoplado: o estado de TodoInputTextField pode ser armazenado em qualquer lugar. Por exemplo, podemos optar por armazenar esse estado em um banco de dados do Room, que é atualizado sempre que um caractere é digitado sem modificar o TodoInputTextField.

Agora, adicione o estado em TodoItemInput e o transmita para TodoInputTextField:

TodoScreen.kt

@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, setText) = remember { mutableStateOf("") }
   Column {
       Row(Modifier
           .padding(horizontal = 16.dp)
           .padding(top = 16.dp)
       ) {
           TodoInputTextField(
               text = text,
               onTextChange = setText,
               modifier = Modifier
                   .weight(1f)
                   .padding(end = 8.dp)
           )
           TodoEditButton(
               onClick = { /* todo */ },
               text = "Add",
               modifier = Modifier.align(Alignment.CenterVertically)
           )
       }
   }
}

Agora, elevado o estado, é possível usar o valor atual do texto para direcionar o comportamento do TodoEditButton. Conclua o callback e enable (ative) o botão somente quando o texto não estiver em branco de acordo com o design:

TodoScreen.kt

// edit TodoItemInput
TodoEditButton(
   onClick = {
       onItemComplete(TodoItem(text)) // send onItemComplete event up
       setText("") // clear the internal text
   },
   text = "Add",
   modifier = Modifier.align(Alignment.CenterVertically),
   enabled = text.isNotBlank() // enable if text is not blank
)

Estamos usando a mesma variável de estado, text, em dois elementos que podem ser compostos diferentes. Ao elevar o estado, podemos compartilhá-lo. Além disso, conseguimos fazer isso tornando apenas TodoItemInput um elemento que pode ser composto com estado.

Executar novamente

Execute o app novamente para ver que agora é possível adicionar itens de tarefas. Parabéns! Você acabou de aprender a adicionar estado a um elemento que pode ser composto e como o elevar.

767719165c35039e.png

Limpeza de código

Antes de continuar, posicione o TodoInputTextField in-line. Adicionamos essa seção para explorar a elevação de estado. Se você analisar o código do TodoInputText que foi fornecido com o codelab, vai ver que ele já está elevado de acordo com os padrões discutidos nesta seção.

Quando você terminar, seu arquivo TodoItemInput vai ter esta aparência:

TodoScreen.kt

@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, setText) = remember { mutableStateOf("") }
   Column {
       Row(Modifier
           .padding(horizontal = 16.dp)
           .padding(top = 16.dp)
       ) {
           TodoInputText(
               text = text,
               onTextChange = setText,
               modifier = Modifier
                   .weight(1f)
                   .padding(end = 8.dp)
           )
           TodoEditButton(
               onClick = {
                   onItemComplete(TodoItem(text))
                   setText("")
               },
               text = "Add",
               modifier = Modifier.align(Alignment.CenterVertically),
               enabled = text.isNotBlank()
           )
       }
   }
}

Na próxima seção, vamos continuar a criar o design e adicionar os ícones. Você vai usar as ferramentas que aprendemos nesta seção para elevar o estado e criar IUs interativas com fluxo de dados unidirecional.

7. IU dinâmica com base no estado

Na última seção, você aprendeu a adicionar um estado a um elemento que pode ser composto e como usar a elevação de estado para criar um elemento que usa outros elementos que podem ser compostos sem estado.

Agora veremos como criar uma IU dinâmica com base no estado. Voltando à simulação do designer, precisamos mostrar a linha do ícone sempre que o texto não estiver em branco.

Entrada do app "Todo" (estado: expandida, texto não em branco) 721446d6a55fcaba.png

Entrada do app "Todo" (estado: recolhida, texto em branco) 6f46071227df3625.png

Derivar iconsVisibles a partir do estado

Abra TodoScreen.kt e crie uma nova variável de estado para armazenar o icon selecionado no momento e uma nova val iconsVisible que seja verdadeira sempre que o texto não estiver em branco.

TodoScreen.kt

@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, setText) = remember { mutableStateOf("") }
   val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
   val iconsVisible = text.isNotBlank()
    // ...

Adicionamos um segundo estado, icon, que contém o ícone selecionado no momento.

O valor iconsVisible não adiciona um novo estado à TodoItemInput. A TodoItemInput não pode ser modificada diretamente. Em vez disso, ela é baseada totalmente no valor de text. Seja qual for o valor do text nessa recomposição, iconsVisible vai ser definido de acordo com isso, e vamos poder usá-lo para mostrar a IU correta.

Podemos adicionar outro estado à TodoItemInput para controlar quando os ícones são visíveis, mas se você observar de perto as especificações, a visibilidade é totalmente baseada no texto inserido. Se criarmos dois estados, vai ser fácil para eles ficarem dessincronizados.

Em vez disso, prefira uma única fonte da verdade. Nesse elemento que pode ser composto, só precisamos que text esteja no estado, e iconsVisible pode se basear em text.

Continue editando a TodoItemInput para mostrar a AnimatedIconRow, dependendo do valor de iconsVisible. Se o valor iconsVisible for verdadeiro, exiba uma AnimatedIconRow. Caso seja falso, exiba um espaçador com 16.dp.

TodoScreen.kt

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height

@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, setText) = remember { mutableStateOf("") }
   val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
   val iconsVisible = text.isNotBlank()
   Column {
       Row( /* ... */ ) {
           /* ... */
       }
       if (iconsVisible) {
           AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
       } else {
           Spacer(modifier = Modifier.height(16.dp))
       }
   }
}

Se você executar o app novamente agora, vai ver que os ícones são animados quando você digita texto.

Estamos mudando dinamicamente a árvore de composição com base no valor do iconsVisible. Veja um diagrama da árvore de composição para ambos os estados.

Esse tipo de lógica de exibição condicional é equivalente à visibilidade no sistema de visualização do Android.

Árvore de composição TodoItemInput quando iconsVisible muda

ceb75cf0f13a1590.png

Se você executar o app novamente, vai ver que a linha do ícone é exibida corretamente, mas, se clicar em "Add", ele não vai aparecer na linha adicionada no app. Isso ocorre porque não atualizamos nosso evento para transmitir o novo estado do ícone, vamos fazer isso a seguir.

Atualizar o evento para usar o ícone

Edite o TodoEditButton na TodoItemInput para usar o novo estado icon no listener onClick.

TodoScreen.kt

TodoEditButton(
   onClick = {
       onItemComplete(TodoItem(text, icon))
       setIcon(TodoIcon.Default)
       setText("")
   },
   text = "Add",
   modifier = Modifier.align(Alignment.CenterVertically),
   enabled = text.isNotBlank()
)

É possível usar o novo estado icon diretamente no listener onClick. Também o redefinimos para o padrão quando o usuário termina de inserir um TodoItem.

Se você executar o app agora, vai ver uma entrada interativa nas tarefas com botões animados. Muito bem!

3d8320f055510332.gif

Concluir o design com uma imeAction

Quando você mostrar o app ao designer, ele vai dizer que o app precisa poder enviar o item da tarefa da ação do IME (editor de método de entrada, na sigla em inglês) usando o teclado. No caso, é o botão azul no canto direito de baixo:

Teclado Android com ImeAction.Done

6ee2444445ec12be.png

TodoInputText permite que você responda à imeAction com o evento onImeAction.

Queremos que a onImeAction tenha exatamente o mesmo comportamento que o TodoEditButton. Poderíamos duplicar o código, mas isso seria difícil de manter ao longo do tempo, porque seria fácil atualizar apenas um dos eventos.

Vamos extrair o evento em uma variável para que possamos usá-lo na onImeAction do TodoInputText e no onClick do TodoEditButton.

Edite TodoItemInput novamente para declarar uma nova função lambda submit, que processa a ação de enviar realizada pelo usuário. Em seguida, transmita a função lambda recém-definida para TodoInputText e TodoEditButton.

TodoScreen.kt

@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, setText) = remember { mutableStateOf("") }
   val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
   val iconsVisible = text.isNotBlank()
   val submit = {
       onItemComplete(TodoItem(text, icon))
       setIcon(TodoIcon.Default)
       setText("")
   }
   Column {
       Row(Modifier
           .padding(horizontal = 16.dp)
           .padding(top = 16.dp)
       ) {
           TodoInputText(
               text = text,
               onTextChange = setText,
               modifier = Modifier
                   .weight(1f)
                   .padding(end = 8.dp),
               onImeAction = submit // pass the submit callback to TodoInputText
           )
           TodoEditButton(
               onClick = submit, // pass the submit callback to TodoEditButton
               text = "Add",
               modifier = Modifier.align(Alignment.CenterVertically),
               enabled = text.isNotBlank()
           )
       }
       if (iconsVisible) {
           AnimatedIconRow(icon, setIcon, Modifier.padding(top = 8.dp))
       } else {
           Spacer(modifier = Modifier.height(16.dp))
       }
   }
}

Se você quiser, pode extrair ainda mais a lógica dessa função. No entanto, essa função de composição está muito boa, então vamos parar por aqui.

Essa é uma das grandes vantagens do Compose. Como você está declarando a IU no Kotlin, é possível criar todas as abstrações necessárias para tornar o código separado e reutilizável.

Para gerenciar o trabalho com o teclado, o TextField oferece dois parâmetros:

  • keyboardOptions: usado para permitir a exibição da ação Done IME
  • keyboardActions: usado para especificar a ação a ser acionada em resposta a ações específicas do IME (editor de método de entrada, na sigla em inglês) acionadas. No nosso caso, depois que "Done" é pressionado, queremos que submit seja chamado e o teclado fique oculto.

Para controlar o teclado de software, vamos usar LocalSoftwareKeyboardController.current. Como esta é uma API experimental, vai ser necessário anotar a função com @OptIn(ExperimentalComposeUiApi::class).

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun TodoInputText(
    text: String,
    onTextChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    onImeAction: () -> Unit = {}
) {
    val keyboardController = LocalSoftwareKeyboardController.current
    TextField(
        value = text,
        onValueChange = onTextChange,
        colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent),
        maxLines = 1,
        keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
        keyboardActions = KeyboardActions(onDone = {
            onImeAction()
            keyboardController?.hide()
        }),
        modifier = modifier
    )
}

Execute o app novamente para testar os novos ícones.

Execute o app novamente para ver os ícones sendo mostrados e ocultados automaticamente enquanto o texto muda. Você também pode mudar a seleção de ícones. Quando você clicar no botão "Add", vai ver um novo TodoItem com base nos valores inseridos.

Parabéns, você aprendeu sobre o estado no Compose, a elevação de estado e como criar IUs dinâmicas com base no estado.

Nas próximas seções, vamos ver como criar componentes reutilizáveis que interagem com o estado.

8. Como extrair elementos sem estado que podem ser compostos

O designer está usando uma nova tendência hoje. A IU desordenada e o pós-Material são coisa do passado. O design desta semana segue a tendência "interativa neomoderna". Você perguntou o que isso significa, e a resposta foi um pouco confusa e envolvia emojis, mas, de qualquer forma, estas são as simulações.

Simulação do modo de edição

O modo de edição reutiliza a mesma IU que o modo de entrada, mas incorpora o editor na lista.

O designer diz que reutiliza a mesma IU que a entrada com os botões modificados para um emoji de "salvar e concluir".

No final da última seção, deixamos TodoItemInput como um elemento que pode ser composto com estado. Isso não era um problema quando se tratava apenas da entrada de novas tarefas no app. No entanto, agora que é um editor, ele vai precisar de suporte para a elevação de estado.

Nesta seção, você vai aprender a extrair um estado de um elemento que pode ser composto com estado para o tornar sem estado. Assim, vamos poder reutilizar o mesmo elemento para adicionar e editar tarefas.

Converter a TodoItemInput em um elemento sem estado que pode ser composto

Para começar, precisamos elevar o estado da TodoItemInput. Mas onde vamos colocá-lo? Podemos colocar o estado diretamente na TodoScreen, mas já está funcionando muito bem com o estado interno e um evento finalizado. Não queremos mudar essa API.

Em vez disso, o que podemos fazer é dividir o elemento que pode ser composto em dois: um que tenha estado e outro que seja sem estado.

Abra TodoScreen.kt e divida a TodoItemInput em dois elementos que podem ser compostos. Em seguida, renomeie o elemento com estado para TodoItemEntryInput, já que ele só é útil para inserir novos TodoItems.

TodoScreen.kt

@Composable
fun TodoItemEntryInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, setText) = remember { mutableStateOf("") }
   val (icon, setIcon) = remember { mutableStateOf(TodoIcon.Default)}
   val iconsVisible = text.isNotBlank()
   val submit = {
       onItemComplete(TodoItem(text, icon))
       setIcon(TodoIcon.Default)
       setText("")
   }
   TodoItemInput(
       text = text,
       onTextChange = setText,
       icon = icon,
       onIconChange = setIcon,
       submit = submit,
       iconsVisible = iconsVisible
   )
}

@Composable
fun TodoItemInput(
   text: String,
   onTextChange: (String) -> Unit,
   icon: TodoIcon,
   onIconChange: (TodoIcon) -> Unit,
   submit: () -> Unit,
   iconsVisible: Boolean
) {
   Column {
       Row(
           Modifier
               .padding(horizontal = 16.dp)
               .padding(top = 16.dp)
       ) {
           TodoInputText(
               text,
               onTextChange,
               Modifier
                   .weight(1f)
                   .padding(end = 8.dp),
               submit
           )
           TodoEditButton(
               onClick = submit,
               text = "Add",
               modifier = Modifier.align(Alignment.CenterVertically),
               enabled = text.isNotBlank()
           )
       }
       if (iconsVisible) {
           AnimatedIconRow(icon, onIconChange, Modifier.padding(top = 8.dp))
       } else {
           Spacer(modifier = Modifier.height(16.dp))
       }
   }
}

Essa transformação é muito importante para entender o Compose. Pegamos um elemento que pode ser composto com estado, TodoItemInput, e o dividimos em dois elementos. Um com estado (TodoItemEntryInput) e outro sem (TodoItemInput).

O elemento que pode ser composto sem estado tem todo o código relacionado à IU, e o elemento com estado não tem nenhum código relacionado à IU. Ao fazer isso, tornamos o código da IU reutilizável em situações em que queremos armazenar o estado de outra forma.

Executar o aplicativo novamente

Execute o aplicativo "TODO" novamente para confirmar que a entrada de novas tarefas ainda funciona.

Parabéns, você extraiu com sucesso um elemento que pode ser composto sem estado de um elemento com estado sem mudar a API dele.

Na próxima seção, veremos como isso pode ser usado para reutilizar a lógica da IU em diferentes locais sem vincular a IU ao estado.

9. Usar estado no ViewModel

Analisando a simulação interativa neomoderna do nosso designer, vai ser necessário adicionar um estado para representar o item de edição atual.

Simulação do modo de edição

O modo de edição reutiliza a mesma IU que o modo de entrada, mas incorpora o editor na lista.

Agora, precisamos decidir onde adicionar o estado para esse editor. Nós podemos criar outro "TodoRowOrInlineEditor" que pode ser composto que gerencia a exibição ou edição de um item, mas só queremos mostrar um editor de cada vez. Analisando o design de perto, a seção de cima também muda no modo de edição. Por isso, faremos uma elevação do estado para permitir que ele seja compartilhado.

Árvore de estado para TodoActivity

d32f2646a3f5ce65.png

Como ambos os elementos TodoItemEntryInput e TodoInlineEditor precisam saber sobre o estado atual do editor para ocultar a entrada na parte de cima da tela, precisamos elevar o estado para no mínimo TodoScreen. A tela é o elemento de nível mais baixo que pode ser composto na hierarquia, além de ser o pai comum de cada elemento que precisa ser editado.

No entanto, como o editor é derivado e vai modificar a lista, ele precisa estar localizado ao lado dela. Queremos elevar o estado para o nível em que ele pode ser modificado. Vamos adicionar a lista em TodoViewModel porque é lá que ela precisa ficar.

Converter o TodoViewModel para usar mutableStateListOf

Nesta seção, você vai adicionar o estado do editor em TodoViewModel e, na próxima seção, vai usá-lo para criar um editor in-line.

Ao mesmo tempo, vamos conhecer mais sobre o uso de mutableStateListOf em um ViewModel e ver como ele simplifica o código de estado em comparação com LiveData<List> ao direcionar o app para o Compose.

mutableStateListOf nos permite criar uma instância observável de MutableList. Isso significa que podemos trabalhar com todoItems da mesma forma que trabalhamos com uma MutableList, removendo a sobrecarga de trabalhar com LiveData<List>.

Abra TodoViewModel.kt e substitua o elemento todoItems existente por um mutableStateListOf:

TodoViewModel.kt

import androidx.compose.runtime.mutableStateListOf

class TodoViewModel : ViewModel() {

   // remove the LiveData and replace it with a mutableStateListOf
   //private var _todoItems = MutableLiveData(listOf<TodoItem>())
   //val todoItems: LiveData<List<TodoItem>> = _todoItems

   // state: todoItems
   var todoItems = mutableStateListOf<TodoItem>()
    private set

   // event: addItem
   fun addItem(item: TodoItem) {
        todoItems.add(item)
   }

   // event: removeItem
   fun removeItem(item: TodoItem) {
       todoItems.remove(item)
   }
}

A declaração de todoItems é curta e captura o mesmo comportamento que a versão de LiveData.

// state: todoItems
var todoItems = mutableStateListOf<TodoItem>()
    private set

Ao especificar private set, estamos restringindo as gravações a esse objeto de estado a um setter privado visível apenas dentro do ViewModel.

Atualizar a TodoActivityScreen para usar o novo ViewModel

Abra TodoActivity.kt e atualize TodoActivityScreen para usar o novo ViewModel.

TodoActivity.kt

@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   TodoScreen(
       items = todoViewModel.todoItems,
       onAddItem = todoViewModel::addItem,
       onRemoveItem = todoViewModel::removeItem
   )
}

Execute o app novamente para ver se ele funciona com o novo ViewModel. Você mudou o estado para usar mutableStateListOf. Agora, vamos ver como criar um estado do editor.

Definir o estado do editor

Agora é hora de adicionar um estado ao nosso editor. Para evitar a duplicação do texto da tarefa, vamos editar a lista diretamente. Para isso, em vez de manter o texto atual que estamos editando, vamos manter um índice de lista com o item atual do editor.

Abra TodoViewModel.kt e adicione um estado de editor.

Defina uma nova private var currentEditPosition que armazena a posição de edição atual. Ela vai conter o índice da lista que estamos editando.

Em seguida, exponha o currentEditItem para fazer a composição usando um getter. Mesmo que essa seja uma função comum do Kotlin, a currentEditPosition é observável para o Compose, assim como um State<TodoItem>.

TodoViewModel.kt

import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue

class TodoViewModel : ViewModel() {

   // private state
   private var currentEditPosition by mutableStateOf(-1)

    // state: todoItems
    var todoItems = mutableStateListOf<TodoItem>()
        private set

   // state
   val currentEditItem: TodoItem?
       get() = todoItems.getOrNull(currentEditPosition)

   // ..

Sempre que uma função de composição chamar currentEditItem, ela vai observar mudanças em todoItems e currentEditPosition. Se uma delas for modificada, o elemento que pode ser composto vai chamar o getter novamente para receber o novo valor.

Definir eventos do editor

Definimos nosso estado de editor. Agora, vamos definir os eventos que os elementos que podem ser compostos podem chamar para controlar a edição.

Crie três eventos: onEditItemSelected(item: TodoItem), onEditDone() e onEditItemChange(item: TodoItem).

Os eventos onEditItemSelected e onEditDone apenas mudam a currentEditPosition. Ao mudar a currentEditPosition, o Compose recompõe qualquer elemento que pode ser composto que lê currentEditItem.

TodoViewModel.kt

class TodoViewModel : ViewModel() {
   ...

   // event: onEditItemSelected
   fun onEditItemSelected(item: TodoItem) {
      currentEditPosition = todoItems.indexOf(item)
   }

   // event: onEditDone
   fun onEditDone() {
      currentEditPosition = -1
   }

   // event: onEditItemChange
   fun onEditItemChange(item: TodoItem) {
      val currentItem = requireNotNull(currentEditItem)
      require(currentItem.id == item.id) {
          "You can only change an item with the same id as currentEditItem"
      }

      todoItems[currentEditPosition] = item
   }
}

O evento onEditItemChange atualiza a lista em currentEditPosition. Isso vai mudar os valores retornados por currentEditItem e todoItems ao mesmo tempo. Antes de fazer isso, há algumas verificações de segurança para garantir que o autor da chamada não esteja tentando gravar o item errado.

Encerrar a edição ao remover itens

Atualize o evento removeItem para fechar o editor atual quando um item for removido.

TodoViewModel.kt

// event: removeItem
fun removeItem(item: TodoItem) {
   todoItems.remove(item)
   onEditDone() // don't keep the editor open when removing items
}

Executar o app novamente

Pronto. Você atualizou o ViewModel para usar o MutableState e viu como ele pode simplificar o código do estado observável.

Na próxima seção, vamos adicionar um teste para esse ViewModel e, depois, criar a IU de edição.

Como houve muitas edições nesta seção, veja uma lista completa do TodoViewModel após a aplicação de todas as mudanças:

TodoViewModel.kt

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel

class TodoViewModel : ViewModel() {

    private var currentEditPosition by mutableStateOf(-1)

    var todoItems = mutableStateListOf<TodoItem>()
        private set

    val currentEditItem: TodoItem?
        get() = todoItems.getOrNull(currentEditPosition)

    fun addItem(item: TodoItem) {
        todoItems.add(item)
    }

    fun removeItem(item: TodoItem) {
        todoItems.remove(item)
        onEditDone() // don't keep the editor open when removing items
    }

    fun onEditItemSelected(item: TodoItem) {
        currentEditPosition = todoItems.indexOf(item)
    }

    fun onEditDone() {
        currentEditPosition = -1
    }

    fun onEditItemChange(item: TodoItem) {
        val currentItem = requireNotNull(currentEditItem)
        require(currentItem.id == item.id) {
            "You can only change an item with the same id as currentEditItem"
        }

        todoItems[currentEditPosition] = item
    }
}

10. Estado de teste no ViewModel

É recomendável testar o ViewModel para verificar se a lógica do aplicativo está correta. Nesta seção, vamos criar um teste para mostrar como testar um modelo de visualização usando State<T> para o estado.

Adicionar um teste a TodoViewModelTest

Abra TodoViewModelTest.kt no diretório test/ e adicione um teste para remover um item:

TodoViewModelTest.kt

import com.example.statecodelab.util.generateRandomTodoItem
import com.google.common.truth.Truth.assertThat
import org.junit.Test

class TodoViewModelTest {

   @Test
   fun whenRemovingItem_updatesList() {
       // before
       val viewModel = TodoViewModel()
       val item1 = generateRandomTodoItem()
       val item2 = generateRandomTodoItem()
       viewModel.addItem(item1)
       viewModel.addItem(item2)

       // during
       viewModel.removeItem(item1)

       // after
       assertThat(viewModel.todoItems).isEqualTo(listOf(item2))
   }
}

Este código mostra como testar o State<T> que é diretamente modificado por eventos. Na seção anterior, ele cria um novo ViewModel e adiciona dois itens a todoItems.

O método que estamos testando é removeItem, que remove o primeiro item da lista.

Por fim, usamos declarações "Truth" para declarar que a lista contém apenas o segundo item.

Não temos que fazer nenhum trabalho extra para ler todoItems em um teste se as atualizações tiverem sido causadas diretamente pelo teste, como estamos fazendo aqui chamando removeItem. Ele é apenas uma List<TodoItem>.

O restante dos testes para este ViewModel segue o mesmo padrão básico. Por isso, ele vai ser ignorado como exercício neste codelab. Você pode adicionar mais testes ao ViewModel para confirmar se ele funciona ou abrir o TodoViewModelTest no módulo concluído para ver mais testes.

Na próxima seção, vamos adicionar o novo modo de edição à IU.

11. Reutilizar elementos sem estado que podem ser compostos

Finalmente, estamos prontos para implementar nosso design interativo neomoderno. Lembre-se que estamos tentando criar algo assim:

Simulação do modo de edição

O modo de edição reutiliza a mesma IU que o modo de entrada, mas incorpora o editor na lista.

Transmitir o estado e os eventos para a TodoScreen

Concluímos a definição completa do estado e dos eventos necessários para essa tela no TodoViewModel. Agora, vamos atualizar a TodoScreen e ver o estado e os eventos necessários para exibir a tela.

Abra TodoScreen.kt e mude a assinatura de TodoScreen para adicionar:

  • O item que está sendo editado no momento: currentlyEditing: TodoItem?
  • Os três novos eventos:

onStartEdit: (TodoItem) -> Unit, onEditItemChange: (TodoItem) -> Unit e onEditDone: () -> Unit.

TodoScreen.kt

@Composable
fun TodoScreen(
   items: List<TodoItem>,
   currentlyEditing: TodoItem?,
   onAddItem: (TodoItem) -> Unit,
   onRemoveItem: (TodoItem) -> Unit,
   onStartEdit: (TodoItem) -> Unit,
   onEditItemChange: (TodoItem) -> Unit,
   onEditDone: () -> Unit
) {
   // ...
}

Esses são apenas o novo estado e o evento que definimos no ViewModel.

Em seguida, no TodoActivity.kt, transmita os novos valores à TodoActivityScreen.

TodoActivity.kt

@Composable
private fun TodoActivityScreen(todoViewModel: TodoViewModel) {
   TodoScreen(
       items = todoViewModel.todoItems,
       currentlyEditing = todoViewModel.currentEditItem,
       onAddItem = todoViewModel::addItem,
       onRemoveItem = todoViewModel::removeItem,
       onStartEdit = todoViewModel::onEditItemSelected,
       onEditItemChange = todoViewModel::onEditItemChange,
       onEditDone = todoViewModel::onEditDone
   )
}

Isso transmite apenas o estado e os eventos que a nova TodoScreen exige.

Definir um editor in-line que pode ser composto

Crie um novo elemento que pode ser composto em TodoScreen.kt, que usa a TodoItemInput que pode ser composta sem estado para definir um editor in-line.

TodoScreen.kt

@Composable
fun TodoItemInlineEditor(
   item: TodoItem,
   onEditItemChange: (TodoItem) -> Unit,
   onEditDone: () -> Unit,
   onRemoveItem: () -> Unit
) = TodoItemInput(
   text = item.task,
   onTextChange = { onEditItemChange(item.copy(task = it)) },
   icon = item.icon,
   onIconChange = { onEditItemChange(item.copy(icon = it)) },
   submit = onEditDone,
   iconsVisible = true
)

Esse elemento que pode ser composto é sem estado. Ele exibe apenas o item transmitido e usa os eventos para solicitar a atualização do estado. Como extraímos um elemento TodoItemInput que pode ser composto sem estado antes, podemos usá-lo facilmente nesse contexto sem estado.

Este exemplo mostra a reutilização de elementos que podem ser compostos sem estado. Mesmo que o cabeçalho use uma TodoItemEntryInput com estado na mesma tela, podemos elevar o estado até o ViewModel do editor in-line.

Usar o editor in-line em LazyColumn

Na LazyColumn da TodoScreen, mostre o TodoItemInlineEditor se o item atual estiver sendo editado. Caso contrário, mostre a TodoRow.

Além disso, comece a editar ao clicar em um item, em vez de o remover como antes.

TodoScreen.kt

// fun TodoScreen()
// ...
LazyColumn(
   modifier = Modifier.weight(1f),
   contentPadding = PaddingValues(top = 8.dp)
) {
 items(items) { todo ->
   if (currentlyEditing?.id == todo.id) {
       TodoItemInlineEditor(
           item = currentlyEditing,
           onEditItemChange = onEditItemChange,
           onEditDone = onEditDone,
           onRemoveItem = { onRemoveItem(todo) }
       )
   } else {
       TodoRow(
           todo,
           { onStartEdit(it) },
           Modifier.fillParentMaxWidth()
       )
   }
 }
}
// ...

O elemento que pode ser composto LazyColumn é o equivalente do Compose a uma RecyclerView. Esse elemento só recompõe os itens na lista necessários para exibir a tela atual. À medida que o usuário rola, os elementos que saíram da tela são descartados e novos elementos são criados para a rolagem.

Experimentar o novo editor interativo

Execute o app novamente. Quando você clicar em uma linha de tarefas do app "Todo", o editor interativo vai ser aberto.

Imagem mostrando o app neste ponto do codelab

Estamos usando a mesma IU sem estado que pode ser composta para desenhar o cabeçalho com estado e a experiência de edição interativa. E não introduzimos nenhum estado duplicado ao fazer isso.

Esse recurso já está começando a tomar forma, mas o botão "Add" parece estar fora do lugar, e precisamos mudar o cabeçalho. Vamos concluir o design nas próximas etapas.

Trocar o cabeçalho ao editar

Em seguida, vamos concluir o design do cabeçalho e ver como trocar o botão pelos botões de emojis que o designer quer para o design interativo neomoderno.

Volte para o elemento que pode ser composto TodoScreen e faça o cabeçalho responder às mudanças no estado do editor. Se currentlyEditing for null, vamos mostrar TodoItemEntryInput e transmitir elevation = true para TodoItemInputBackground. Se currentlyEditing não for null, transmita elevation = false para TodoItemInputBackground e exiba o texto "Editing item" (editando item) no mesmo plano de fundo.

TodoScreen.kt

import androidx.compose.material.MaterialTheme
import androidx.compose.ui.text.style.TextAlign

@Composable
fun TodoScreen(
   items: List<TodoItem>,
   currentlyEditing: TodoItem?,
   onAddItem: (TodoItem) -> Unit,
   onRemoveItem: (TodoItem) -> Unit,
   onStartEdit: (TodoItem) -> Unit,
   onEditItemChange: (TodoItem) -> Unit,
   onEditDone: () -> Unit
) {
   Column {
       val enableTopSection = currentlyEditing == null
       TodoItemInputBackground(elevate = enableTopSection) {
           if (enableTopSection) {
               TodoItemEntryInput(onAddItem)
           } else {
               Text(
                   "Editing item",
                   style = MaterialTheme.typography.h6,
                   textAlign = TextAlign.Center,
                   modifier = Modifier
                       .align(Alignment.CenterVertically)
                       .padding(16.dp)
                       .fillMaxWidth()
               )
           }
       }
      // ..

Novamente, estamos mudando a árvore do Compose na recomposição. Quando a parte de cima é ativada, mostramos a TodoItemEntryInput. Caso contrário, mostramos um Text que pode ser composto e com o texto "Editing item" (editando item).

O TodoItemInputBackground que estava no código inicial anima automaticamente o redimensionamento e também as mudanças de elevação. Assim, quando você entrar no modo de edição, esse código vai ser animado automaticamente entre os estados.

Executar o app novamente

99c4d82c8df52606.gif

Execute o app novamente para ver que ele é animado entre os estados de edição e não edição. O design está quase concluído.

Na próxima seção, vamos ver como estruturar o código dos botões de emoji.

12. Usar slots para transmitir seções da tela

Elementos que podem ser compostos sem estado que exibem IUs complexas podem acabar com muitos parâmetros. Se não forem muitos parâmetros e se eles configurarem diretamente o elemento que pode ser composto, tudo bem. No entanto, às vezes você precisa transmitir parâmetros para configurar os filhos de um elemento que pode ser composto.

O design com o botão &quot;Add&quot; (adicionar) na barra de ferramentas e botões de emojis no editor in-line

No design interativo neomoderno, o designer quer que o botão "Add" (adicionar) fique na parte de cima, mas ele precisa ser trocado por dois botões "emoji" para o editor in-line. Nós poderíamos adicionar mais parâmetros à TodoItemInput para lidar com esses casos, mas não está claro se isso é realmente a responsabilidade da TodoItemInput.

O que precisamos é uma maneira de um elemento que pode ser composto usar uma seção de botões pré-configurada. Com isso, o autor da chamada vai poder configurar os botões sem precisar compartilhar todo o estado necessário para os configurar com a TodoItemInput.

Isso vai reduzir o número de parâmetros transmitidos a elementos que podem ser compostos sem estado e os tornará mais reutilizáveis.

O padrão para transmitir uma seção pré-configurada é o de slots. Slots são parâmetros de um elemento que pode ser composto que permitem que o autor da chamada descreva uma seção da tela. Você vai ver exemplos de slots em todas as APIs integradas que podem ser compostas. Um dos exemplos mais usados é Scaffold.

Scaffold é o elemento que pode ser composto para descrever uma tela inteira no Material Design, como topBar, bottomBar e o corpo da tela.

Em vez de fornecer centenas de parâmetros para configurar cada seção da tela, o Scaffold expõe os slots que você pode preencher com qualquer função de composição. Isso reduz o número de parâmetros para Scaffold e facilita a reutilização. Se você quiser criar uma topBar personalizada, ela vai ser mostrada pelo Scaffold.

@Composable
fun Scaffold(
   // ..
   topBar: @Composable (() -> Unit)? = null,
   bottomBar: @Composable (() -> Unit)? = null,
   // ..
   bodyContent: @Composable (PaddingValues) -> Unit
) {

Definir um slot no TodoItemInput

Abra TodoScreen.kt e defina um novo parâmetro @Composable () -> Unit com o nome buttonSlot na TodoItemInput.

TodoScreen.kt

@Composable
fun TodoItemInput(
   text: String,
   onTextChange: (String) -> Unit,
   icon: TodoIcon,
   onIconChange: (TodoIcon) -> Unit,
   submit: () -> Unit,
   iconsVisible: Boolean,
   buttonSlot: @Composable () -> Unit
) {
  // ...

Esse é um slot genérico que o autor da chamada pode preencher com os botões desejados. Ele vai ser usado para especificar botões diferentes para os cabeçalhos e os editores in-line.

Exibir o conteúdo de buttonSlot

Substitua a chamada de TodoEditButton pelo conteúdo do slot.

TodoScreen.kt

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width

@Composable
fun TodoItemInput(
   text: String,
   onTextChange: (String) -> Unit,
   icon: TodoIcon,
   onIconChange: (TodoIcon) -> Unit,
   submit: () -> Unit,
   iconsVisible: Boolean,
   buttonSlot: @Composable() () -> Unit,
) {
   Column {
       Row(
           Modifier
               .padding(horizontal = 16.dp)
               .padding(top = 16.dp)
       ) {
           TodoInputText(
               text,
               onTextChange,
               Modifier
                   .weight(1f)
                   .padding(end = 8.dp),
               submit
           )

           // New code: Replace the call to TodoEditButton with the content of the slot

           Spacer(modifier = Modifier.width(8.dp))
           Box(Modifier.align(Alignment.CenterVertically)) { buttonSlot() }

           // End new code
       }
       if (iconsVisible) {
           AnimatedIconRow(icon, onIconChange, Modifier.padding(top = 8.dp))
       } else {
           Spacer(modifier = Modifier.height(16.dp))
       }
   }
}

Podemos chamar buttonSlot() diretamente, mas precisamos manter o align para centralizar o que o autor da chamada nos transmite verticalmente. Para fazer isso, colocamos o slot em uma Box, que é um elemento que pode ser composto básico.

Atualizar a TodoItemEntryInput com estado para usar o slot

Agora, precisamos atualizar os autores das chamadas para usar o buttonSlot. Primeiro, vamos atualizar a TodoItemEntryInput:

TodoScreen.kt

@Composable
fun TodoItemEntryInput(onItemComplete: (TodoItem) -> Unit) {
   val (text, onTextChange) = remember { mutableStateOf("") }
   val (icon, onIconChange) = remember { mutableStateOf(TodoIcon.Default)}

   val submit = {
        if (text.isNotBlank()) {
            onItemComplete(TodoItem(text, icon))
            onTextChange("")
            onIconChange(TodoIcon.Default)
        }
   }
   TodoItemInput(
       text = text,
       onTextChange = onTextChange,
       icon = icon,
       onIconChange = onIconChange,
       submit = submit,
       iconsVisible = text.isNotBlank()
   ) {
       TodoEditButton(onClick = submit, text = "Add", enabled = text.isNotBlank())
   }
}

Como o buttonSlot é o último parâmetro para a TodoItemInput, podemos usar a sintaxe de lambda final. Em seguida, na lambda, chame TodoEditButton como fizemos antes.

Atualizar o TodoItemInlineEditor para usar o slot

Para concluir a refatoração, mude TodoItemInlineEditor para usar o slot também:

TodoScreen.kt

import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.TextButton

@Composable
fun TodoItemInlineEditor(
   item: TodoItem,
   onEditItemChange: (TodoItem) -> Unit,
   onEditDone: () -> Unit,
   onRemoveItem: () -> Unit
) = TodoItemInput(
   text = item.task,
   onTextChange = { onEditItemChange(item.copy(task = it)) },
   icon = item.icon,
   onIconChange = { onEditItemChange(item.copy(icon = it)) },
   submit = onEditDone,
   iconsVisible = true,
   buttonSlot = {
       Row {
           val shrinkButtons = Modifier.widthIn(20.dp)
           TextButton(onClick = onEditDone, modifier = shrinkButtons) {
               Text(
                   text = "\uD83D\uDCBE", // floppy disk
                   textAlign = TextAlign.End,
                   modifier = Modifier.width(30.dp)
               )
           }
           TextButton(onClick = onRemoveItem, modifier = shrinkButtons) {
               Text(
                   text = "❌",
                   textAlign = TextAlign.End,
                   modifier = Modifier.width(30.dp)
               )
           }
       }
   }
)

Aqui, transmitimos o buttonSlot como um parâmetro nomeado. Em seguida, em buttonSlot, criamos uma linha contendo os dois botões para o design do editor in-line.

Executar o app novamente

Execute o app novamente e teste o editor in-line.

ae3f79834a615ed0.gif

Nesta seção, personalizamos nosso elemento que pode ser composto sem estado usando um slot, o que permitiu que o autor da chamada controlasse uma seção da tela. Ao usar slots, evitamos acoplar a TodoItemInput a todos os designs diferentes que podem ser adicionados no futuro.

Quando você perceber que está adicionando parâmetros a elementos que podem ser compostos sem estado para personalizar os filhos, avalie se o uso de slots seria melhor. Os slots tendem a tornar os elementos que podem ser compostos mais reutilizáveis, mantendo o número de parâmetros gerenciáveis.

13. Parabéns

Parabéns! Você concluiu este codelab e aprendeu a estruturar o estado usando o fluxo de dados unidirecional em um app do Jetpack Compose.

Você aprendeu a pensar em estados e eventos para extrair elementos sem estado que podem ser compostos no Compose e viu como reutilizar um elemento complexo em diferentes situações na mesma tela. Você também aprendeu a integrar um ViewModel ao Compose usando o LiveData e o MutableState.

Qual é a próxima etapa?

Confira os outros codelabs no programa de aprendizagem do Compose:

Apps de exemplo (link em inglês)

  • O JetNews mostra como usar um fluxo de dados unidirecional para elementos que podem ser compostos com estado e gerenciar o estado deles em uma tela criada com elementos sem estado.

Documentos de referência