Introdução ao estado no Compose

1. Antes de começar

Este codelab ensina sobre o estado e como ele pode ser usado e manipulado pelo Jetpack Compose.

Basicamente, o estado em um app é qualquer valor que pode mudar ao longo do tempo. Essa é uma definição bem ampla que inclui tudo, desde um banco de dados até uma variável no app. Você vai saber mais sobre os bancos de dados em outra unidade. Por enquanto, basta saber que eles são coleções organizadas de informações estruturadas, como arquivos do seu computador.

Todos os apps Android mostram o estado para o usuário. Veja alguns exemplos de estado em apps Android:

  • Uma mensagem que mostra quando não é possível estabelecer uma conexão de rede.
  • Formulários, como formulários de registro. O estado pode ser preenchido e enviado.
  • Controles em que o usuário pode tocar, como botões. O estado pode ser não tocado, em processo de toque (animação na tela) ou tocado (uma ação onClick).

Neste codelab, você vai aprender a usar e avaliar o estado ao usar o Compose. Para isso, vamos criar um app de calculadora de gorjetas chamado Tip Time com estes elementos de interface integrados no Compose:

  • Um elemento combinável TextField para inserir e editar texto.
  • Um elemento combinável Text para mostrar texto
  • Um elemento combinável Spacer para mostrar espaços vazios entre os elementos da interface.

Ao final deste codelab, você terá criado uma calculadora de gorjetas interativa que calcula automaticamente o valor da gorjeta ao receber o valor do serviço. Esta imagem mostra a aparência do app final:

e82cbb534872abcf.png

Pré-requisitos

  • Noções básicas do Compose, como a anotação @Composable.
  • Conhecimento básico dos layouts do Compose, como os elementos combináveis de layout Row e Column.
  • Conhecimento básico sobre modificadores, como a função Modifier.padding().
  • Familiaridade com o elemento combinável Text.

O que você vai aprender

  • Como pensar sobre o estado em uma interface.
  • Como o Compose usa o estado para mostrar dados.
  • Como adicionar uma caixa de texto ao app.
  • Como elevar um estado.

O que você vai criar

  • Um app de calculadora de gorjetas chamado Tip Time que calcula o valor da gorjeta com base no valor do serviço.

O que é necessário

  • Um computador com acesso à Internet e um navegador da Web.
  • Conhecimento sobre Kotlin.
  • A versão mais recente do Android Studio.

2. Começar

  1. Acesse a calculadora on-line de gorjetas do Google. Este é apenas um exemplo e não é o app Android que você vai criar neste curso.

46bf4366edc1055f.png 18da3c120daa0759.png

  1. Insira valores diferentes nas caixas Bill (conta) e Tip % (porcentagem da gorjeta). O valor da gorjeta e o total mudam de acordo com as informações inseridas.

c0980ba3e9ebba02.png

Quando você informa os valores, as informações em Tip e Total são atualizadas. No final do codelab a seguir, você vai desenvolver um app de calculadora de gorjetas semelhante no Android.

Neste módulo, você vai criar um app simples de calculadora de gorjetas para o Android.

Os desenvolvedores geralmente trabalham assim: primeiro, criam uma versão simples e funcional do app, mesmo que ela não tenha uma aparência muito boa, e depois adicionam recursos e a deixam mais bonita.

Ao final deste codelab, seu app de calculadora de gorjetas vai ficar parecido com as capturas de tela. Quando o usuário inserir o valor de uma conta, seu app vai mostrar um valor de gorjeta sugerido. Por enquanto, a porcentagem da gorjeta está fixada em 15%. No próximo codelab, você continuará trabalhando no app e vai adicionar outros recursos, como uma porcentagem personalizada.

3. Receber código inicial

O código inicial é um código pré-escrito que pode ser usado como ponto de partida para um novo projeto. Com ele, você pode se concentrar nos novos conceitos ensinados neste codelab.

Faça o download do código inicial para começar:

Outra opção é clonar o repositório do GitHub:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout starter

Procure o código inicial no repositório do GitHub do TipTime (link em inglês).

Visão geral do app inicial

Para se familiarizar com o código inicial, siga estas etapas:

  1. Abra o projeto com o código inicial no Android Studio.
  2. Execute o app em um dispositivo Android ou emulador.
  3. Há dois componentes de texto: um deles para um rótulo, e o outro para mostrar o valor da gorjeta.

e85b767a43c69a97.png

Código inicial etapa por etapa

O código inicial tem os elementos combináveis de texto. Neste Programa de treinamentos, você vai adicionar um campo de texto para receber entradas dos usuários. Confira a seguir um breve tutorial sobre alguns arquivos para começar.

res > values > strings.xml

<resources>
   <string name="app_name">Tip Time</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="bill_amount">Bill Amount</string>
   <string name="tip_amount">Tip Amount: %s</string>
</resources>

Este é o arquivo string.xml nos recursos com todas as strings que você vai usar neste app.

MainActivity

Esse arquivo contém principalmente o código gerado por modelo e as funções a seguir.

  • A função TipTimeLayout() contém um elemento Column com dois elementos combináveis de texto que aparecem nas capturas de tela. Ela também tem o combinável spacer para adicionar espaço por motivos estéticos.
  • A função calculateTip() aceita o valor da conta e calcula um valor de gorjeta de 15%. O parâmetro tipPercent é definido como um valor de argumento padrão 15.0. Com isso, você vai definir o valor de gorjeta padrão como 15%, por enquanto. No próximo codelab, você vai saber o valor da gorjeta do usuário.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(R.string.calculate_tip),
            modifier = Modifier
                .padding(bottom = 16.dp, top = 40.dp)
                .align(alignment = Alignment.Start)
        )
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        Spacer(modifier = Modifier.height(150.dp))
    }
}
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
   val tip = tipPercent / 100 * amount
   return NumberFormat.getCurrencyInstance().format(tip)
}

No bloco Surface() da função onCreate(), a função TipTimeLayout() está sendo chamada. Com isso, o layout do app é exibido no dispositivo ou emulador.

override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeLayout()
           }
       }
   }
}

No bloco TipTimeTheme da função TipTimeLayoutPreview(), a função TipTimeLayout() está sendo chamada. Com isso, o layout do app é exibido no Design e no painel Split.

@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
   TipTimeTheme {
       TipTimeLayout()
   }
}

ae11354e61d2a2b9.png

Receber entrada do usuário

Nesta seção, você vai adicionar o elemento de interface que permite inserir o valor do serviço no app. Confira a aparência dele:

58671affa01fb9e1.png

O app usa um estilo e um tema personalizados.

Estilos e temas são uma coleção de atributos que especificam a aparência de um único elemento da interface. Um estilo pode especificar atributos que podem ser aplicados a todo o app, como cor e tamanho da fonte, cor do plano de fundo e muito mais. Outros codelabs vão abordar a implementação esses atributos no seu app. Por enquanto, isso já foi feito para deixar seu app mais bonito.

Para entender melhor, confira uma comparação lado a lado da versão de solução do aplicativo com e sem um tema personalizado.

Sem um tema personalizado.

Com um tema personalizado.

A função combinável TextField permite que o usuário insira texto em um app. Por exemplo, observe a caixa de texto na tela de login do app Gmail nesta imagem:

Tela de um smartphone com o app Gmail aberto e mostrando um campo de texto para e-mail

Adicione o elemento combinável TextField ao app:

  1. No arquivo MainActivity.kt, adicione uma função combinável EditNumberField(), que usa um parâmetro Modifier.
  2. No corpo da função EditNumberField() abaixo de TipTimeLayout(), adicione um TextField que aceite um parâmetro chamado value, definido como uma string vazia e um parâmetro chamado onValueChange definido como uma expressão lambda vazia:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   TextField(
      value = "",
      onValueChange = {},
      modifier = modifier
   )
}
  1. Observe os parâmetros que você transmitiu:
  • O parâmetro value é uma caixa de texto que mostra o valor da string que você transmite aqui.
  • O parâmetro onValueChange é o callback lambda acionado quando o usuário insere texto na caixa.
  1. Importe esta função:
import androidx.compose.material3.TextField
  1. No combinável TipTimeLayout(), na linha após a primeira função combinável de texto, chame a função EditNumberField(), transmitindo o modificador a seguir.
import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun TipTimeLayout() {
   Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
   ) {
       Text(
           ...
       )
       EditNumberField(modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth())
       Text(
           ...
       )
       ...
   }
}

A caixa de texto será mostrada na tela.

  1. No painel Design, aparecem o texto Calculate Tip, uma caixa de texto vazia e o elemento combinável Tip Amount.

2c208378cd4b8d41.png

4. Usar o estado no Compose

O estado em um app é qualquer valor que pode mudar ao longo do tempo. Neste app, o estado é o valor da conta.

Adicione uma variável para armazenar o estado:

  1. No início da função EditNumberField(), use a palavra-chave val para adicionar uma variável amountInput e defini-la com o valor "0":
val amountInput = "0"

Este é o estado do app para o valor da conta.

  1. Defina o parâmetro chamado value com um valor amountInput:
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. Verificar a visualização. A caixa de texto mostra o valor definido para a variável de estado, como mostrado nesta imagem:

e8e24821adfd9d8c.png

  1. Execute o app no emulador e tente inserir um valor diferente. O estado fixado no código continua o mesmo, porque o elemento combinável TextField não é atualizado automaticamente. Ele muda quando o parâmetro value, definido como a propriedade amountInput, é modificado.

A variável amountInput representa o estado da caixa de texto. Ter um estado fixado no código não é útil, já que ele não pode ser modificado e não reflete a entrada do usuário. É necessário atualizar o estado do app quando o usuário atualiza o valor da conta.

5. A combinação

Os elementos combináveis no app descrevem uma interface que mostra uma coluna com texto, um espaçador e uma caixa de texto. O texto mostra Calculate Tip, e a caixa de texto mostra um valor 0 ou o valor padrão.

O Compose é um framework de interface declarativo, ou seja, você declara como será a interface no código. Se você quiser que a caixa de texto mostre inicialmente um valor 100, defina no código o valor inicial dos elementos de composição como 100.

O que acontece se você quiser que a interface mude enquanto o app está em execução ou quando o usuário interage com ele? Por exemplo, o que acontece quando você atualiza a variável amountInput com o valor inserido pelo usuário e a mostra na caixa de texto? É então que você precisa usar um processo chamado recomposição para atualizar a composição do app.

A composição é uma descrição da interface criada pelo Compose quando ele executa elementos combináveis. Os apps do Compose chamam funções combináveis para transformar dados em elementos da interface. Se uma mudança de estado ocorrer, o Compose vai executar novamente as funções combináveis afetadas com o novo estado, criando uma interface atualizada. Isso é chamado de recomposição. O Compose programa uma recomposição para você.

Quando o Compose executa seus elementos combináveis pela primeira vez durante a composição inicial, ele acompanha os elementos chamados para descrever a interface em uma composição. A recomposição acontece quando o Compose executa novamente os elementos de composição que foram modificados em resposta a mudanças de dados e, em seguida, atualiza a composição para refletir essas mudanças.

A composição só pode ser produzida por uma composição inicial e atualizada por recomposição. A única maneira de modificar uma composição é pela recomposição. Para fazer isso, o Compose precisa saber qual estado será acompanhado para programar a recomposição ao receber uma atualização. No seu caso, é a variável amountInput. Então, sempre que o valor mudar, o Compose vai programar uma recomposição.

Você usa os tipos State e MutableState no Compose para tornar o estado no app observável ou monitorado pelo Compose. O tipo State é imutável, ou seja, o valor dele só será lido, enquanto o tipo MutableState for mutável. Você pode usar a função mutableStateOf() para criar um MutableState observável. Ele recebe um valor inicial como um parâmetro envolvido por um objeto State, que torna o value dele observável.

O valor retornado pela função mutableStateOf():

  • mantém o estado, que é o valor da conta;
  • é mutável, então o valor pode ser mudado;
  • é observável, então o Compose observa as mudanças no valor e aciona uma recomposição para atualizar a interface.

Adicione um estado de custo de serviço:

  1. Na função EditNumberField(), mude a palavra-chave val antes da variável de estado amountInput para a palavra-chave var:
var amountInput = "0"

Isso torna o amountInput mutável.

  1. Use o tipo MutableState<String> em vez da variável String fixada no código para que o Compose saiba que precisa acompanhar o estado amountInput e depois transmitir uma string "0", que é o valor padrão inicial para a variável de estado amountInput:
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

var amountInput: MutableState<String> = mutableStateOf("0")

A inicialização de amountInput também pode ser programada desta forma com inferência de tipo:

var amountInput = mutableStateOf("0")

A função mutableStateOf() recebe um valor inicial de "0" como argumento, fazendo com que o amountInput seja observável. O resultado é este aviso de compilação no Android Studio, mas isso poderá ser corrigido em breve:

Creating a state object during composition without using remember.
  1. Na função combinável TextField, use a propriedade amountInput.value:
TextField(
   value = amountInput.value,
   onValueChange = {},
   modifier = modifier
)

O Compose monitora cada elemento combinável que lê propriedades de estado value e aciona uma recomposição quando o value é modificado.

O callback onValueChange é acionado quando a entrada na caixa de texto é mudada. Na expressão lambda, a variável it contém o novo valor.

  1. Na expressão lambda do parâmetro chamado onValueChange, defina a propriedade amountInput.value como a variável it:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput = mutableStateOf("0")
   TextField(
       value = amountInput.value,
       onValueChange = { amountInput.value = it },
       modifier = modifier
   )
}

Você está atualizando o estado de TextField (a variável amountInput), quando TextField notifica que há uma mudança no texto usando a função de callback onValueChange.

  1. Execute o app e digite o texto. A caixa de texto ainda mostra um valor 0, como nesta imagem:

3a2c62f8ec55e339.gif

Quando o usuário digita na caixa de texto, o callback onValueChange é chamado, e a variável amountInput é atualizada com o novo valor. O estado amountInput é acompanhado pelo Compose. Assim, quando o valor muda, a recomposição é programada, e a função de composição EditNumberField() é executada novamente. Nessa função de composição, a variável amountInput é redefinida para o valor inicial de 0. Assim, a caixa de texto mostra um valor 0.

Com o código adicionado, as mudanças de estado fazem com que as recomposições sejam programadas.

No entanto, é necessário preservar o valor da variável amountInput nas recomposições para que ela não seja redefinida como 0 sempre que a função EditNumberField() é recomposta. Vamos resolver esse problema na próxima seção.

6. Usar a função remember para salvar o estado

Os métodos de composição podem ser chamados várias vezes graças à recomposição. Se não for salvo, o elemento de composição vai redefinir o estado durante a recomposição.

As funções de composição podem armazenar um objeto nas recomposições com a função remember. Um valor calculado pela função remember é armazenado na composição durante a composição inicial e é retornado durante a recomposição. Normalmente, as funções remember e mutableStateOf são usadas em conjunto nas funções combináveis para que o estado e as atualizações dele sejam refletidos corretamente na interface.

Use a função remember na EditNumberField():

  1. Na função EditNumberField(), inicialize a variável amountInput com o delegado de propriedade by remember do Kotlin, envolvendo a chamada para a função mutableStateOf() com remember.
  2. Na função mutableStateOf(), transmita uma string vazia em vez de uma string "0" estática:
var amountInput by remember { mutableStateOf("") }

Agora, a string vazia é o valor padrão inicial da variável amountInput. by é uma delegação de propriedade do Kotlin (link em inglês). As funções getter e setter padrão da propriedade amountInput são delegadas às funções getter e setter da classe remember, respectivamente.

  1. Importe estas funções:
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

A adição das importações getter e setter do delegado permite ler e definir amountInput sem referenciar a propriedade value do MutableState.

A função EditNumberField() atualizada vai ficar assim:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       modifier = modifier
   )
}
  1. Execute o app e digite na caixa de texto. O texto digitado é mostrado agora.

59ac301a208b47c4.png

7. Estado e recomposição em ação

Nesta seção, você vai definir um ponto de interrupção e depurar a função de composição EditNumberField() para ver como a composição inicial e a recomposição funcionam.

Defina um ponto de interrupção e depure o app em um emulador ou dispositivo:

  1. Na função EditNumberField(), ao lado do parâmetro chamado onValueChange, defina um ponto de interrupção de linha.
  2. No menu de navegação, clique em Debug 'app'. O app será iniciado no emulador ou dispositivo. A execução do app é pausada pela primeira vez quando o elemento TextField é criado.

154e060231439307.png

  1. No painel Debug, clique em 2a29a3bad712bec.png Resume Program. A caixa de texto será criada.
  2. No emulador ou dispositivo, digite uma letra na caixa de texto. A execução do app é pausada novamente quando atinge o ponto de interrupção definido.

Quando você insere o texto, o callback onValueChange é chamado. Dentro da lambda, it tem o novo valor que você digitou no teclado.

Depois que o valor de "it" é atribuído a amountInput, o Compose aciona a recomposição com os novos dados, já que o valor observável mudou.

1d5e08d32052d02e.png

  1. No painel Debug, clique em 2a29a3bad712bec.png Resume Program. O texto inserido no emulador ou no dispositivo é mostrado ao lado da linha com o ponto de interrupção, conforme mostrado nesta imagem:

1f5db6ab5ca5b477.png

Esse é o estado do campo de texto.

  1. Clique em 2a29a3bad712bec.png Resume Program. O valor inserido é exibido no emulador ou no dispositivo.

8. Modificar a aparência

Na seção anterior, você configurou o funcionamento do campo de texto. Nesta, vamos melhorar a interface.

Adicionar um rótulo à caixa de texto

Cada caixa de texto precisa ter um rótulo que informe aos usuários quais informações eles podem inserir. Na primeira parte da imagem de exemplo a seguir, o texto do rótulo fica no meio de um campo de texto e alinhado à linha de entrada. Na segunda parte, o rótulo é movido mais para cima na caixa de texto quando o usuário clica nela. Para saber mais sobre a anatomia do campo de texto, consulte Anatomia (link em inglês).

a2afd6c7fc547b06.png

Modifique a função EditNumberField() para adicionar um rótulo ao campo de texto:

  1. Na função de composição TextField() da função EditNumberField(), adicione um parâmetro chamado label definido como uma expressão lambda vazia:
TextField(
//...
   label = { }
)
  1. Na expressão lambda, chame a função Text() que aceita um stringResource(R.string.bill_amount):
label = { Text(stringResource(R.string.bill_amount)) },
  1. Na função combinável TextField(), adicione o parâmetro chamado singleLine como um valor true:
TextField(
  // ...
   singleLine = true,
)

Essa ação condensa a caixa de texto em uma única linha rolável horizontalmente.

  1. Adicione o parâmetro keyboardOptions definido como um KeyboardOptions():
import androidx.compose.foundation.text.KeyboardOptions

TextField(
  // ...
   keyboardOptions = KeyboardOptions(),
)

O Android oferece uma opção para configurar o teclado mostrado na tela para inserir dígitos, endereços de e-mail, URLs, senhas, entre outros. Para saber mais sobre outros tipos de teclado, consulte KeyboardType.

  1. Defina o tipo de teclado como teclado numérico para inserir dígitos. Transmita a função KeyboardOptions com um parâmetro chamado keyboardType definido como KeyboardType.Number:
import androidx.compose.ui.text.input.KeyboardType

TextField(
  // ...
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)

A função EditNumberField() concluída vai ficar parecida com este snippet de código:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
    var amountInput by remember { mutableStateOf("") }
    TextField(
        value = amountInput,
        onValueChange = { amountInput = it },
        singleLine = true,
        label = { Text(stringResource(R.string.bill_amount)) },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        modifier = modifier
    )
}
  1. Execute o app.

Confira as mudanças no teclado nesta captura de tela:

55936268bf007ee9.png

9. Mostrar o valor da gorjeta

Nesta seção, você vai implementar a funcionalidade principal do app, que é a capacidade de calcular e mostrar o valor da gorjeta.

No arquivo MainActivity.kt, uma função private calculateTip() é fornecida como parte do código inicial. Use essa função para calcular o valor da gorjeta:

private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
    val tip = tipPercent / 100 * amount
    return NumberFormat.getCurrencyInstance().format(tip)
}

No método acima, você está usando NumberFormat para exibir o formato da gorjeta como moeda.

Agora, seu app pode calcular a gorjeta, mas ela ainda precisa ser formatada e mostrada com a classe.

Usar a função calculateTip()

O texto inserido pelo usuário no elemento de composição de campo de texto é retornado à função de callback onValueChange como uma String, mesmo que o usuário tenha inserido um número. Para corrigir isso, você precisa converter o valor amountInput, que contém a quantia inserida pelo usuário.

  1. Na função combinável EditNumberField(), crie uma nova variável com o nome amount após a definição de amountInput. Chame a função toDoubleOrNull na variável amountInput para converter String em Double:
val amount = amountInput.toDoubleOrNull()

A função toDoubleOrNull() é uma função predefinida do Kotlin que analisa uma string como um número Double e retorna o resultado ou null caso a string não seja uma representação válida de um número.

  1. No final da instrução, adicione um operador Elvis ?: que retorne um valor 0.0 quando a amountInput for nula:
val amount = amountInput.toDoubleOrNull() ?: 0.0
  1. Após a variável amount, crie outra variável val chamada tip. Inicialize com calculateTip(), transmitindo o parâmetro amount.
val tip = calculateTip(amount)

A função EditNumberField() vai ficar parecida com este snippet de código:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       label = { Text(stringResource(R.string.bill_amount)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
   )
}

Mostrar o valor da gorjeta calculado

Você programou a função para calcular o valor da gorjeta. A próxima etapa é exibir o valor calculado:

  1. Na função TipTimeLayout(), no final do bloco Column(), observe o elemento combinável de texto que exibe $0.00. Você vai atualizar esse valor para o valor calculado da gorjeta.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // ...
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        // ...
    }
}

Você precisa acessar a variável amountInput na função TipTimeLayout() para calcular e mostrar o valor da gorjeta. No entanto, a variável amountInput é o estado do campo de texto definido na função combinável EditNumberField(). Ela ainda não pode ser chamada pela função TipTimeLayout(). Esta imagem ilustra a estrutura do código:

50bf0b9d18ede6be.png

Essa estrutura não permite que você mostre o valor da gorjeta no novo elemento combinável Text, já que o elemento Text precisa acessar a variável amount calculada com a variável amountInput. Você precisa expor a variável amount à função TipTimeLayout(). Esta imagem ilustra a estrutura de código desejada, que faz com que o elemento combinável EditNumberField() não tenha estado:

ab4ec72388149f7c.png

Esse padrão é chamado de elevação de estado. Na próxima seção, você vai elevar o estado de um elemento combinável para que ele fique sem estado.

10. Elevação de estado

Nesta seção, você vai aprender onde definir seu estado para reutilizar e compartilhar seus elementos de composição.

Em uma função combinável, você pode definir variáveis que contenham o estado para exibição na interface. Por exemplo, você definiu a variável amountInput como estado no elemento de composição EditNumberField().

Quando o app fica mais complexo e outros elementos de composição precisam de acesso ao estado dentro do elemento EditNumberField(), é necessário elevar, ou extrair, o estado da função de composição EditNumberField().

Elementos de composição com e sem estado

Você vai precisar elevar o estado sempre que for necessário:

  • compartilhar o estado com várias funções de composição;
  • criar um elemento combinável sem estado para ser reutilizado no app.

Quando você extrai o estado de uma função combinável, a função resultante é chamada de sem estado. Ou seja, as funções combináveis podem ficar sem estado ao terem o estado extraído.

Um elemento combinável sem estado é aquele que não contém nenhum estado, ou seja, que não armazena, define nem modifica um novo estado. Por outro lado, um combinável com estado tem um estado que pode mudar ao longo do tempo.

Elevação de estado é um padrão para mover um estado para cima e fazer com que um componente passe a não ter estado.

Quando aplicado a elementos combináveis, esse processo geralmente introduz dois parâmetros ao elemento:

  • Um parâmetro value: T, que é o valor atual a ser mostrado.
  • Um lambda de callback onValueChange: (T) -> Unit, que é acionado quando o valor muda para que o estado possa ser atualizado em outro lugar, como quando um usuário digita na caixa de texto.

Eleve o estado na função EditNumberField():

  1. Atualize a definição da função EditNumberField() para elevar o estado adicionando os parâmetros value e onValueChange:
@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
//...

O parâmetro value é do tipo String, e o onValueChange é do tipo (String) -> Unit. Portanto, essa é uma função que usa um valor String como entrada e não tem valor de retorno. O parâmetro onValueChange é usado como o callback onValueChange transmitido no combinável TextField.

  1. Na função EditNumberField(), atualize a função combinável TextField() para usar os parâmetros transmitidos:
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. Faça uma elevação, movendo o estado lembrado da função EditNumberField() para a TipTimeLayout():
@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)
  
   Column(
       //...
   ) {
       //...
   }
}
  1. Você elevou o estado para TipTimeLayout(). Agora, ele precisa ser transmitido para EditNumberField(). Na função TipTimeLayout(), atualize a chamada de função EditNumberField() para usar o estado elevado:
EditNumberField(
   value = amountInput,
   onValueChange = { amountInput = it },
   modifier = Modifier
       .padding(bottom = 32.dp)
       .fillMaxWidth()
)

Isso deixa EditNumberField sem estado. Você elevou o estado da interface para seu ancestral, TipTimeLayout(). TipTimeLayout() é o proprietário do estado (amountInput) agora.

Formatação posicional

A formatação posicional é usada para exibir conteúdo dinâmico em strings. Por exemplo, vamos supor que você queira que a caixa de texto Tip amount (Valor da gorjeta) mostre um valor xx.xx, que pode ser qualquer valor calculado e formatado na sua função. Para fazer isso no arquivo strings.xml, você precisa definir o recurso de string com um argumento marcador de posição, como este snippet de código:

// No need to copy.

// In the res/values/strings.xml file
<string name="tip_amount">Tip Amount: %s</string>

No código de combinação, você pode ter vários argumentos de marcador e de qualquer tipo. Um marcador string é %s.

Observe o elemento de combinável de texto em TipTimeLayout(). Transmita a gorjeta formatada como um argumento para a função stringResource().

// No need to copy
Text(
   text = stringResource(R.string.tip_amount, "$0.00"),
   style = MaterialTheme.typography.displaySmall
)
  1. Na função, TipTimeLayout(), use a propriedade tip para exibir o valor da gorjeta. Atualize o parâmetro text do elemento combinável Text para usar a variável tip como parâmetro.
Text(
     text = stringResource(R.string.tip_amount, tip),
     // ...

As funções TipTimeLayout() e EditNumberField() concluídas serão semelhantes a este snippet de código:

@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }
   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
       horizontalAlignment = Alignment.CenterHorizontally,
       verticalArrangement = Arrangement.Center
   ) {
       Text(
           text = stringResource(R.string.calculate_tip),
           modifier = Modifier
               .padding(bottom = 16.dp, top = 40.dp)
               .align(alignment = Alignment.Start)
       )
       EditNumberField(
           value = amountInput,
           onValueChange = { amountInput = it },
           modifier = Modifier
               .padding(bottom = 32.dp)
               .fillMaxWidth()
       )
       Text(
           text = stringResource(R.string.tip_amount, tip),
           style = MaterialTheme.typography.displaySmall
       )
       Spacer(modifier = Modifier.height(150.dp))
   }
}

@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   TextField(
       value = value,
       onValueChange = onValueChange,
       singleLine = true,
       label = { Text(stringResource(R.string.bill_amount)) },
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
       modifier = modifier
   )
}

Resumindo, você elevou o estado amountInput do EditNumberField() para o elemento de composição TipTimeLayout(). Para que a caixa de texto funcione como antes, é necessário transmitir dois argumentos à função combinável EditNumberField(): o valor amountInput e o callback lambda que atualiza o valor amountInput com base na entrada do usuário. Essas mudanças permitem calcular a gorjeta da propriedade amountInput no TipTimeLayout() para que ela seja mostrada ao usuário.

  1. Execute o app no emulador ou dispositivo e insira um valor na caixa de texto bill amount (valor da gorjeta). O valor de 15% do valor da conta é mostrado como na imagem:

de593783dc813e24.png

11. Acessar o código da solução

Para fazer o download do código do codelab concluído, use estes comandos git:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout state

Se preferir, você pode fazer o download do repositório como um arquivo ZIP, descompactar e abrir no Android Studio.

Se você quiser conferir o código da solução, acesse o GitHub (em inglês).

12. Conclusão

Parabéns! Você concluiu este codelab e aprendeu a usar o estado em um app do Compose.

Resumo

  • O estado em um app é qualquer valor que pode mudar ao longo do tempo.
  • A composição é uma descrição da interface criada pelo Compose quando ele executa elementos combináveis. Os apps do Compose chamam funções combináveis para transformar dados em elementos da interface.
  • A composição inicial é uma criação da interface pelo Compose ao executar funções combináveis pela primeira vez.
  • A recomposição é o processo de executar novamente os mesmos elementos combináveis para atualizar a árvore quando os dados deles mudam.
  • Elevação de estado é um padrão para mover um estado para cima e fazer com que um componente passe a não ter estado.

Saiba mais