Calcular uma gorjeta personalizada

1. Antes de começar

Neste codelab, você vai usar o código da solução do codelab Introdução ao estado no Compose para criar uma calculadora interativa de gorjetas que pode calcular e arredondar um valor ao inserir a conta e a porcentagem da gorjeta. Confira o app final nesta imagem:

d8e768525099378a.png

Pré-requisitos

  • Ter feito o codelab Introdução ao estado no Compose.
  • Saber adicionar os elementos combináveis Text e TextField a um app.
  • Conhecimento sobre a função remember(), estado, elevação de estado e diferença entre funções combináveis com e sem estado.

O que você vai aprender

  • Como adicionar um botão de ação a um teclado virtual.
  • O que é um elemento combinável Switch e como usá-lo.
  • Adicionar ícones principais aos campos de texto.

O que você vai criar

  • Um app Tip Time que calcula valores de gorjetas com base no valor da conta inserido pelo usuário e na porcentagem da gorjeta.

O que é necessário

2. Acessar o código inicial

Para começar, faça o download do código inicial:

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 state

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

3. Visão geral do app inicial

Este codelab começa com o app Tip Time do codelab anterior, Introdução ao estado no Compose, que fornece a interface do usuário necessária para calcular uma gorjeta com uma porcentagem fixa. A caixa de texto Bill amount (valor da conta) permite que o usuário insira o preço do serviço. O app calcula e mostra o valor da gorjeta em um elemento combinável Text.

Executar o app Tip Time

  1. Abra o projeto Tip Time no Android Studio e execute o app em um emulador ou dispositivo.
  2. Insira o valor da conta. O app calcula e mostra automaticamente o valor da gorjeta.

b6bd5374911410ac.png

Na implementação atual, a porcentagem da gorjeta está fixada no código como 15%. Neste codelab, você vai estender esse recurso com um campo de texto que permite ao app calcular uma porcentagem personalizada e arredondar o valor da gorjeta.

Adicionar os recursos de string necessários

  1. Na guia Project, clique em res > values > strings.xml.
  2. Entre as tags <resources> do arquivo strings.xml, adicione estes recursos de string:
<string name="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string>

O arquivo strings.xml será parecido com o snippet de código abaixo, que inclui as strings do codelab anterior:

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="how_was_the_service">Tip Percentage</string>
    <string name="round_up_tip">Round up tip?</string>
    <string name="tip_amount">Tip Amount: %s</string>
</resources>

4. Adicionar um campo de texto de porcentagem da gorjeta

É possível que um cliente queira aumentar ou diminuir a gorjeta com base na qualidade do serviço prestado e por vários outros motivos. Para acomodar isso, o app precisa permitir que o usuário calcule uma gorjeta personalizada. Nesta seção, você vai adicionar um campo de texto para que o usuário insira uma porcentagem da gorjeta, como mostrado nesta imagem:

391b4b1a090687ef.png

Você já tem um campo de texto Bill amount (valor da conta) no app, que é a função combinável EditNumberField() sem estado. No codelab anterior, você elevou o estado amountInput do elemento combinável EditNumberField() para o combinável TipTimeLayout(), deixando o elemento EditNumberField() sem estado.

Para adicionar um campo de texto, reutilize o mesmo elemento combinável EditNumberField(), mas com um rótulo diferente. Para fazer essa mudança, você precisa transmitir o rótulo como um parâmetro em vez de codificá-lo na função combinável EditNumberField().

Torne a função combinável EditNumberField() reutilizável:

  1. No arquivo MainActivity.kt nos parâmetros da função combinável EditNumberField(), adicione um recurso de string label do tipo Int:
@Composable
fun EditNumberField(
    label: Int,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
)
  1. No corpo da função, substitua o ID do recurso de string fixado no código pelo parâmetro label:
@Composable
fun EditNumberField(
    //...
) {
     TextField(
         //...
         label = { Text(stringResource(label)) },
         //...
     )
}
  1. Para indicar que o parâmetro label é uma referência de recurso de string, faça a anotação @StringRes no parâmetro da função:
@Composable
fun EditNumberField(
    @StringRes label: Int,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
) 
  1. Importe o seguinte:
import androidx.annotation.StringRes
  1. Na chamada EditNumberField() da função combinável TipTimeLayout(), defina o parâmetro label como o recurso de string R.string.bill_amount:
EditNumberField(
    label = R.string.bill_amount,
    value = amountInput,
    onValueChanged = { amountInput = it },
    modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
  1. No painel Preview, não deve haver nenhuma mudança visual.

b223d5ba4a54f792.png

  1. Na função combinável TipTimeLayout() após a chamada EditNumberField(), adicione outro campo de texto para a porcentagem de gorjeta personalizada. Chame a função combinável EditNumberField() com estes parâmetros:
EditNumberField(
    label = R.string.how_was_the_service,
    value = "",
    onValueChanged = { },
    modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)

Isso adiciona outra caixa de texto para a porcentagem de gorjeta personalizada.

  1. A visualização do app agora mostra um campo de texto Tip Percentage (porcentagem da gorjeta), como nesta imagem:

a5f5ef5e456e185e.png

  1. Na parte de cima da função combinável TipTimeLayout(), adicione uma propriedade var com o nome tipInput para a variável de estado do campo de texto adicionado. Use mutableStateOf("") para inicializar a variável e cercar a chamada com a função remember:
var tipInput by remember { mutableStateOf("") }
  1. Na nova chamada de função EditNumberField(), defina o parâmetro com nome value como a variável tipInput e atualize a variável tipInput na expressão lambda onValueChanged:
EditNumberField(
    label = R.string.how_was_the_service,
    value = tipInput,
    onValueChanged = { tipInput = it },
    modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
  1. Na função TipTimeLayout(), depois da definição da variável tipInput. Defina um val com o nome tipPercent que converte a variável tipInput em um tipo Double. Use um operador Elvis e retorne 0 se o valor for null. Esse valor poderá ser null se o campo de texto estiver vazio.
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
  1. Na função TipTimeLayout(), atualize a chamada calculateTip() e transmita a variável tipPercent como o segundo parâmetro:
val tip = calculateTip(amount, tipPercent)

O código da função TipTimeLayout() vai ficar parecido com este snippet de código:

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

    val tip = calculateTip(amount, tipPercent)
    Column(
        modifier = Modifier.padding(40.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(R.string.calculate_tip),
            modifier = Modifier
                .padding(bottom = 16.dp)
                .align(alignment = Alignment.Start)
        )
        EditNumberField(
            label = R.string.bill_amount,
            value = amountInput,
            onValueChanged = { amountInput = it },
            modifier = Modifier
                .padding(bottom = 32.dp)
                .fillMaxWidth()
        )
        EditNumberField(
            label = R.string.how_was_the_service,
            value = tipInput,
            onValueChanged = { tipInput = 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))
    }
}
  1. Execute o app em um emulador ou dispositivo e insira o valor da conta e a porcentagem da gorjeta. O app calcula o valor da gorjeta corretamente?

captura de tela com o valor da conta em 100 dólares, a porcentagem da gorjeta em 20% e o valor da gorjeta em 20 dólares

5. Definir um botão de ação

No codelab anterior, você explorou como usar a classe KeyboardOptions para definir o tipo do teclado. Nesta seção, você vai aprender a definir o botão de ação do teclado com as mesmas KeyboardOptions. Um botão de ação do teclado é um botão no final dele. Confira alguns exemplos nesta tabela:

Propriedade

Botão de ação no teclado

ImeAction.Search
Usada quando o usuário quer realizar uma pesquisa.

A imagem mostra o ícone de lupa para fazer uma pesquisa.

ImeAction.Send
Usada quando o usuário quer enviar o texto no campo de entrada.

A imagem mostra o ícone para enviar o texto inserido no campo de entrada.

ImeAction.Go
Usada quando o usuário quer navegar até o destino do texto na entrada.

A imagem representa o ícone do avançar para navegar até o destino do texto inserido no campo de entrada.

Nesta tarefa, você vai definir dois botões de ação diferentes para as caixas de texto:

  • Um botão de ação Next (próximo) para a caixa de texto Bill Amount (valor da conta), que indica que o usuário terminou a entrada atual e quer passar para a próxima caixa de texto.
  • Um botão de ação Done (concluído) para a caixa de texto Tip Percentage, que indica que o usuário terminou de digitar a gorjeta.

Confira exemplos de teclados com esses botões de ação nestas imagens:

Adicione opções de teclado:

  1. Na chamada TextField() da função EditNumberField(), transmita ao construtor KeyboardOptions um argumento com o nome imeAction definido como um valor ImeAction.Next. Use a função KeyboardOptions.Default.copy() para garantir o uso das outras opções padrão.
import androidx.compose.ui.text.input.ImeAction

@Composable
fun EditNumberField(
    //...
) {
    TextField(
        //...
        keyboardOptions = KeyboardOptions.Default.copy(
            keyboardType = KeyboardType.Number,
            imeAction = ImeAction.Next
        )
    )
}
  1. Execute o app em um emulador ou dispositivo. O teclado agora mostra o botão de ação Next, como você pode conferir nesta imagem:

82574a95b658f052.png

O teclado mostra o mesmo botão de ação Next quando o campo de texto Tip Percentage está selecionado. No entanto, é recomendável ter dois botões de ação diferentes nos campos de texto. Esse problema vai ser corrigido em breve.

  1. Examine a função EditNumberField(). O parâmetro keyboardOptions na função TextField() está fixado no código. Para criar botões de ação diferentes para os campos de texto, transmita o objeto KeyboardOptions como um argumento. Isso vai ser feito na próxima etapa.
// No need to copy, just examine the code.
fun EditNumberField(
    @StringRes label: Int,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    TextField(
        //...
        keyboardOptions = KeyboardOptions.Default.copy(
           keyboardType = KeyboardType.Number,
           imeAction = ImeAction.Next
        )
    )
}
  1. Na definição da função EditNumberField(), adicione um parâmetro keyboardOptions do tipo KeyboardOptions. No corpo da função, atribua-o ao parâmetro keyboardOptions da função TextField():
@Composable
fun EditNumberField(
    @StringRes label: Int,
    keyboardOptions: KeyboardOptions,
    // ...
){
    TextField(
        //...
        keyboardOptions = keyboardOptions
    )
}
  1. Na função TipTimeLayout(), atualize a primeira chamada de função EditNumberField() e transmita o parâmetro keyboardOptions para o campo de texto Bill Amount:
EditNumberField(
    label = R.string.bill_amount,
    keyboardOptions = KeyboardOptions.Default.copy(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Next
    ),
    // ...
)
  1. Na segunda chamada de função EditNumberField(), mude a imeAction do campo de texto Tip Percentage para ImeAction.Done. A função será semelhante a este snippet de código:
EditNumberField(
    label = R.string.how_was_the_service,
    keyboardOptions = KeyboardOptions.Default.copy(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Done
    ),
    // ...
)
  1. Execute o app. Ele mostra os botões de ação Next e Done, como você pode ver nestas imagens:

  1. Digite qualquer valor para a conta e clique no botão de ação Next. Insira qualquer porcentagem de gorjeta e clique no botão de ação Done. Isso fecha o teclado.

a9e3fbddfff829c8.gif

6. Adicionar uma chave

Uma chave ativa ou desativa o estado de um único item.

6923dfb1101602c7.png

Há dois estados em um botão de alternância que permitem ao usuário selecionar entre duas opções. Um botão de alternância consiste em uma faixa, um círculo e um ícone opcional, como mostrado nestas imagens:

b4f7f68b848bcc2b.png

Uma chave é um controle de seleção que pode ser usado para inserir decisões ou declarar preferências, por exemplo, configurações, como aparece nesta imagem:

5cd8acb912ab38eb.png

O usuário pode arrastar o círculo para frente e para trás a fim de escolher a opção selecionada ou simplesmente tocar na chave para alternar. Este GIF mostra outro exemplo de um botão de ativação, em que a configuração "Visual options" (opções visuais) muda para o Dark mode (modo escuro):

eabf96ad496fd226.gif

Para saber mais, consulte a documentação sobre chaves.

Use o elemento combinável Switch para que o usuário arredonde a gorjeta para o número inteiro mais próximo, como você pode conferir nesta imagem:

b42af9f2d3861e4.png

Adicione uma linha para os elementos combináveis Text e Switch:

  1. Depois da função EditNumberField(), adicione uma função combinável RoundTheTipRow() e transmita um Modifier padrão, como argumentos semelhantes à função EditNumberField():
@Composable
fun RoundTheTipRow(modifier: Modifier = Modifier) {
}
  1. Implemente a função RoundTheTipRow(), adicione um elemento combinável de layout Row com o modifier abaixo para definir a largura dos elementos filhos como o máximo na tela, centralize o alinhamento e garanta um tamanho de 48dp:
Row(
   modifier = modifier
       .fillMaxWidth()
       .size(48.dp),
   verticalAlignment = Alignment.CenterVertically
) {
}
  1. Importe o seguinte:
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
  1. No bloco lambda do elemento combinável do layout Row, adicione um elemento Text que use o recurso de string R.string.round_up_tip para mostrar uma string Round up tip?:
Text(text = stringResource(R.string.round_up_tip))
  1. Depois do elemento combinável Text, adicione um elemento Switch e transmita um parâmetro checked definido como roundUp e um parâmetro onCheckedChange definido como onRoundUpChanged.
Switch(
    checked = roundUp,
    onCheckedChange = onRoundUpChanged,
)

Esta tabela contém informações sobre esses parâmetros, que são os mesmos definidos para a função RoundTheTipRow():

Parâmetro

Descrição

checked

Se a chave está marcada ou não. Esse é o estado do elemento combinável Switch.

onCheckedChange

O callback que é chamado quando a chave recebe um clique.

  1. Importe o seguinte:
import androidx.compose.material3.Switch
  1. Na função RoundTheTipRow(), adicione um parâmetro roundUp do tipo Boolean e uma função lambda onRoundUpChanged que usa um Boolean e não retorna nada:
@Composable
fun RoundTheTipRow(
    roundUp: Boolean,
    onRoundUpChanged: (Boolean) -> Unit,
    modifier: Modifier = Modifier
)

Isso eleva o estado da chave.

  1. No elemento combinável Switch, adicione este modifier para alinhar o elemento Switch ao final da tela:
       Switch(
           modifier = modifier
               .fillMaxWidth()
               .wrapContentWidth(Alignment.End),
           //...
       )
  1. Importe o seguinte:
import androidx.compose.foundation.layout.wrapContentWidth
  1. Na função TipTimeLayout(), adicione uma variável var para o estado do elemento combinável Switch. Crie uma variável var com o nome roundUp e a defina como mutableStateOf(), com false como o valor inicial. Envolva a chamada com remember { }.
fun TipTimeLayout() {
    //...
    var roundUp by remember { mutableStateOf(false) }

    //...
    Column(
        ...
    ) {
      //...
   }
}

Essa é a variável do estado do elemento combinável Switch e "false" será o estado padrão.

  1. No bloco Column da função TipTimeLayout(), depois do campo de texto Tip Percentage, chame a função RoundTheTipRow() com estes argumentos: parâmetro roundUp definido como roundUp e um parâmetro nomeado onRoundUpChanged definido como um callback lambda que atualiza o valor roundUp:
@Composable
fun TipTimeLayout() {
    //...

    Column(
        ...
    ) {
        Text(
            ...
        )
        Spacer(...)
        EditNumberField(
            ...
        )
        EditNumberField(
            ...
        )
        RoundTheTipRow(
             roundUp = roundUp,
             onRoundUpChanged = { roundUp = it },
             modifier = Modifier.padding(bottom = 32.dp)
         )
        Text(
            ...
        )
    }
}

A linha Round up tip? (arredondar gorjeta?) vai aparecer.

  1. Execute o app. O botão Round up tip? vai aparecer.

5225395a29022a5e.png

  1. Insira o valor da conta e a porcentagem da gorjeta e selecione a opção Round up tip?. O valor da gorjeta não é arredondado porque você ainda precisa atualizar a função calculateTip(), o que vai ser feito na próxima seção.

Atualizar a função calculateTip() para arredondar a gorjeta

Modifique a função calculateTip() para aceitar uma variável Boolean e arredondar (link em inglês) a gorjeta para o número inteiro mais próximo:

  1. Para arredondar a gorjeta, a função calculateTip() precisa saber o estado da chave, que é um Boolean. Na função calculateTip(), adicione um parâmetro roundUp do tipo Boolean:
private fun calculateTip(
    amount: Double,
    tipPercent: Double = 15.0,
    roundUp: Boolean
): String { 
    //...
}
  1. Na função calculateTip(), antes da instrução return, adicione uma condição if() que verifica o valor roundUp. Se roundUp for true, defina uma variável tip como a função kotlin.math.ceil() e, em seguida, transmita a função tip como argumento:
if (roundUp) {
    tip = kotlin.math.ceil(tip)
}

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

private fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
    var tip = tipPercent / 100 * amount
    if (roundUp) {
        tip = kotlin.math.ceil(tip)
    }
    return NumberFormat.getCurrencyInstance().format(tip)
}
  1. Na função TipTimeLayout(), atualize a chamada calculateTip() e transmita um parâmetro roundUp:
val tip = calculateTip(amount, tipPercent, roundUp)
  1. Execute o app. Agora, o valor da gorjeta vai ser arredondado, como você pode ver nestas imagens:

7. Adicionar suporte à orientação paisagem

Os dispositivos Android têm vários formatos, como smartphones, tablets, dispositivos dobráveis e com Chrome OS, com uma ampla variedade de tamanhos de tela. Seu app deve oferecer suporte às orientações retrato e paisagem.

  1. Teste seu app no modo paisagem e ative o giro automático.

8566fc367d5a5b2f.png

  1. Gire o emulador ou dispositivo para a esquerda. O valor da gorjeta não aparece. Para resolver esse problema, você precisa de uma barra de rolagem vertical para rolar a tela do app.

28d23a73c2a5ea24.png

  1. Adicione .verticalScroll(rememberScrollState()) ao modificador para ativar a rolagem vertical da coluna. O rememberScrollState() cria e lembra automaticamente do estado de rolagem.
@Composable
fun TipTimeLayout() {
    // ...
    Column(
        modifier = Modifier
            .padding(40.dp)
            .verticalScroll(rememberScrollState()),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        //...
    }
}
  1. Importe o seguinte:
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
  1. Execute o app novamente. Role a tela no modo paisagem.

179866a0fae00401.gif

8. Adicionar ícone principal a campos de texto (opcional)

Os ícones podem tornar o campo de texto mais atraente visualmente e fornecer informações adicionais sobre o campo de texto. Os ícones podem ser usados para transmitir informações sobre a finalidade do campo de texto, por exemplo, o tipo de dados esperado ou o tipo de entrada necessária. Por exemplo, o ícone de um telefone ao lado de um campo de texto pode indicar que o usuário deve inserir um número de telefone.

Os ícones podem ser usados para orientar a entrada do usuário, fornecendo indicações visuais sobre o que é esperado. Por exemplo, o ícone de um calendário ao lado de um campo de texto pode indicar que o usuário deve inserir uma data.

O exemplo a seguir mostra um campo de texto com um ícone de pesquisa, indicando a inserção do termo de pesquisa.

9318c9a2414c4add.png

Adicione outro parâmetro ao combinável EditNumberField() com o nome leadingIcon do tipo Int. Anote-o com @DrawableRes.

@Composable
fun EditNumberField(
    @StringRes label: Int,
    @DrawableRes leadingIcon: Int,
    keyboardOptions: KeyboardOptions,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
) 
  1. Importe o seguinte:
import androidx.annotation.DrawableRes
import androidx.compose.material3.Icon
  1. Adicione o ícone inicial ao campo de texto. O leadingIcon usa um combinável, que será transmitido no seguinte combinável Icon.
TextField(
    value = value,
    leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
    //...
)
  1. Transmita o ícone principal para os campos de texto. Para sua conveniência, os ícones já estão presentes no código inicial.
EditNumberField(
    label = R.string.bill_amount,
    leadingIcon = R.drawable.money,
    // Other arguments
)
EditNumberField(
    label = R.string.how_was_the_service,
    leadingIcon = R.drawable.percent,
    // Other arguments
)
  1. Execute o app.

bff007b9d67ede83.png

Parabéns! Agora seu app pode calcular gorjetas personalizadas.

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

Para fazer o download do código do codelab concluído, use este comando git:

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

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 (link em inglês).

10. Conclusão

Parabéns! Você adicionou a função de gorjeta personalizada ao app Tip Time. Agora, o app permite que os usuários digitem uma porcentagem de gorjeta personalizada e arredondem o valor final. Compartilhe seu trabalho nas mídias sociais usando a hashtag #AndroidBasics.

Saiba mais