Navegar entre telas com o Compose

1. Antes de começar

Até agora, os apps com que você trabalhou consistiam em uma única tela. No entanto, muitos dos apps que você usa provavelmente têm várias telas em que é possível navegar. Por exemplo, o app Configurações tem muitas páginas de conteúdo espalhadas por diferentes telas.

No Modern Android Development, apps multitelas são criados usando o componente Navigation do Jetpack. O componente Navigation do Compose permite criar apps multitelas no Compose com facilidade usando uma abordagem declarativa, assim como a criação de interfaces do usuário. Este codelab apresenta os fundamentos do componente Navigation do Compose, como tornar a AppBar responsiva e como enviar dados do seu app para outro usando intents, tudo isso enquanto demonstra as práticas recomendadas em um app cada vez mais complexo.

Pré-requisitos

  • Conhecer a linguagem Kotlin, incluindo tipos de função, lambdas e funções de escopo.
  • Familiaridade com layouts básicos de Row e Column no Compose.

O que você aprenderá

  • Criar um elemento combinável do NavHost para definir rotas e telas no seu app.
  • Navegar entre telas usando um NavHostController.
  • Manipular a backstack para voltar às telas anteriores.
  • Usar intents para compartilhar dados com outro app.
  • Personalizar a AppBar, incluindo o título e o botão "Voltar".

O que você vai criar

  • Você vai implementar a navegação em um app multitelas.

O que é necessário

  • A versão mais recente do Android Studio.
  • Conexão de Internet para fazer o download do código inicial.

2. Fazer o download do 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-cupcake.git
$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout starter

Confira o código inicial deste codelab no GitHub (link em inglês).

3. Tutorial do app

O app Cupcake é um pouco diferente dos apps com que você trabalhou até agora. Em vez de todo o conteúdo ser mostrado em uma única tela, o app tem quatro telas separadas e o usuário pode navegar em cada uma enquanto pede cupcakes. Se você executar o app, nada vai aparecer e você não poderá navegar entre essas telas, já que o componente Navigation ainda não foi adicionado ao código do app. No entanto, ainda é possível verificar as visualizações combináveis para cada tela e combiná-las com as telas finais do app abaixo.

Tela de início do pedido

A primeira tela apresenta ao usuário três botões que correspondem à quantidade de cupcakes por pedido.

No código, isso é representado pelo elemento combinável StartOrderScreen em StartOrderScreen.kt.

A tela consiste em uma única coluna, com imagem e texto, além de três botões personalizados para pedir diferentes quantidades de cupcakes. Os botões personalizados são implementados pelo elemento SelectQuantityButton, que também está em StartOrderScreen.kt.

Tela de escolha do sabor

Depois de selecionar a quantidade, o app solicita que o usuário selecione um sabor de cupcake. O app usa o que é conhecido como botões de opção para mostrar diferentes sabores. O usuário pode selecionar um sabor entre todos os possíveis.

A lista de possíveis sabores é armazenada como uma lista de IDs de recursos de string em data.DataSource.kt.

Tela de escolha da data de retirada

Depois de escolher um sabor, o app apresenta ao usuário outra série de botões de opção para selecionar uma data de retirada. As opções de retirada vêm de uma lista retornada pela função pickupOptions() em OrderViewModel.

As telas Choose Flavor (escolha o sabor) e Choose Pickup Date (escolha a data de retirada) são representadas pelo mesmo elemento combinável, SelectOptionScreen em SelectOptionScreen.kt. Por que usar o mesmo elemento combinável? O layout dessas telas é exatamente o mesmo. A única diferença são os dados, mas você pode usar o mesmo elemento combinável para mostrar as telas de sabor e de data de retirada.

Tela de resumo do pedido

Após selecionar a data de retirada, o app vai mostrar a tela Order Summary (resumo do pedido), em que o usuário pode analisar e concluir o pedido.

Essa tela é implementada pelo elemento combinável OrderSummaryScreen em SummaryScreen.kt.

O layout consiste em uma Column que contém todas as informações sobre o pedido, um elemento Text combinável para o subtotal e os botões para enviar o pedido para outro app ou cancelar e retornar à primeira tela.

Se os usuários optarem por enviar o pedido para outro app, o app Cupcake vai mostrar uma Android ShareSheet com diferentes opções de compartilhamento.

a32e016a6ccbf427.png

O estado atual do app é armazenado em data.OrderUiState.kt. A classe de dados OrderUiState contém propriedades para armazenar as seleções do usuário de cada tela.

As telas do app são apresentadas no elemento combinável CupcakeApp. No entanto, no projeto inicial, o app simplesmente mostra a primeira tela. No momento, não é possível navegar em todas as telas do app, mas não se preocupe. É para isso que você está aqui. Você vai aprender a definir rotas de navegação, configurar um NavHost de composição para navegar entre telas (também conhecidas como destinos), executar intents para integrar componentes da IU do sistema (como a tela de compartilhamento) e fazer a AppBar responder a mudanças de navegação.

Elementos de composição reutilizáveis

Quando adequado, os apps de exemplo deste curso foram criados para implementar práticas recomendadas. O app Cupcake não é exceção. No pacote ui.components, você vai ver um arquivo chamado CommonUi.kt com um elemento combinável FormattedPriceLabel. Várias telas no app usam esse elemento para formatar o preço do pedido de maneira consistente. Em vez de duplicar o mesmo elemento combinável Text com a mesma formatação e modificadores, você pode definir FormattedPriceLabel uma vez e reutilizá-lo para outras telas quantas vezes forem necessárias.

As telas de sabor e de data de retirada usam o elemento SelectOptionScreen, que também é reutilizável. Esse elemento combinável usa um parâmetro chamado options do tipo List<String>, que representa as opções a serem mostradas. As opções aparecem em uma Row, que consiste em um elemento combinável RadioButton e um elemento Text que contém cada string. Uma Column envolve todo o layout e também contém um elemento combinável Text para mostrar o preço formatado, um botão Cancel e um botão Next.

4. Definir rotas e criar um NavHostController

Partes do componente de navegação

O componente de navegação tem três partes principais:

  • NavController: responsável por navegar entre os destinos, ou seja, as telas do seu app.
  • NavGraph: mapeia os destinos de composição para navegar.
  • NavHost: elemento combinável que funciona como um contêiner para mostrar o destino atual do NavGraph.

Neste codelab, vamos nos concentrar no NavController e no NavHost. No NavHost, você vai definir os destinos do NavGraph do app Cupcake.

Definir rotas para destinos no seu app

Um dos conceitos fundamentais de navegação em um app do Compose é a rota. Uma rota é uma string correspondente a um destino. Essa ideia é semelhante ao conceito de URL. Assim como um URL diferente mapeia para outra página em um site, uma rota é uma string que mapeia para um destino e serve como seu identificador exclusivo. Um destino normalmente é um único elemento ou um grupo de elementos de composição correspondentes ao que o usuário vê. O app Cupcake precisa de destinos para as telas de início do pedido, de sabor, de data de retirada e de resumo do pedido.

Há um número finito de telas em um app, então também há um número finito de rotas. É possível definir as rotas de um app usando uma classe de enumeração. As classes de enumeração no Kotlin têm uma propriedade de nome que retorna uma string com o nome da propriedade.

Para começar, defina as quatro rotas do app Cupcake.

  • Start: selecione um dos três botões para selecione a quantidade de cupcakes.
  • Flavor: selecione o sabor em uma lista de opções.
  • Pickup: selecione a data de retirada em uma lista de opções.
  • Summary: revise as seleções e envie ou cancele o pedido.

Adicione uma classe de enumeração para definir as rotas.

  1. No CupcakeScreen.kt, acima do elemento combinável CupcakeAppBar, adicione uma classe de enumeração com o nome CupcakeScreen.
enum class CupcakeScreen() {

}
  1. Adicione quatro casos à classe de enumeração: Start, Flavor, Pickup e Summary.
enum class CupcakeScreen() {
    Start,
    Flavor,
    Pickup,
    Summary
}

Adicionar um NavHost ao seu app

Um NavHost é um elemento combinável que mostra outros destinos combinável, com base em uma determinada rota. Por exemplo, se a rota for Flavor, o NavHost vai mostrar a tela de escolha do sabor do cupcake. Se a rota for Summary, o app vai mostrar a tela de resumo.

A sintaxe do NavHost é igual a qualquer outro elemento combinável.

fae7688d6dd53de9.png

Há dois parâmetros importantes.

  • navController: uma instância da classe NavHostController. É possível usar esse objeto para navegar entre telas, por exemplo, chamando o método navigate() para navegar para outro destino. Você pode buscar o NavHostController chamando rememberNavController() em uma função de composição.
  • startDestination: uma rota de string que define o destino mostrado por padrão quando o app mostra o NavHost pela primeira vez. No caso do app Cupcake, é a rota Start.

Como outros elementos combináveis, o NavHost também usa um parâmetro modifier.

Você vai adicionar um NavHost ao elemento CupcakeApp no CupcakeScreen.kt. Primeiro, você precisa de uma referência para o controlador de navegação. Você pode usar o controlador de navegação tanto no NavHost adicionado quanto na AppBar que vai ser adicionada em uma próxima etapa. Portanto, declare a variável no elemento combinável CupcakeApp().

  1. Abra o CupcakeScreen.kt.
  2. No Scaffold, abaixo da variável uiState, adicione um elemento combinável NavHost.
import androidx.navigation.compose.NavHost

Scaffold(
    ...
) { innerPadding ->
    val uiState by viewModel.uiState.collectAsState()

    NavHost()
}
  1. Transmita a variável navController ao parâmetro navController e CupcakeScreen.Start.name ao parâmetro startDestination. Transmita o modificador que foi transmitido ao CupcakeApp() para o parâmetro modificador. Transmita um lambda final vazio para o parâmetro final.
import androidx.compose.foundation.layout.padding

NavHost(
    navController = navController,
    startDestination = CupcakeScreen.Start.name,
    modifier = Modifier.padding(innerPadding)
) {

}

Processar rotas no NavHost

Como outros elementos combináveis, o NavHost usa um tipo de função para o próprio conteúdo.

f67974b7fb3f0377.png

Na função de conteúdo de um NavHost, você chama a função composable(). A função composable() tem dois parâmetros obrigatórios.

  • route: uma string correspondente ao nome de uma rota. Ela pode ser qualquer string exclusiva. Você vai usar a propriedade de nome das constantes de enumeração CupcakeScreen.
  • content: aqui é possível chamar um elemento combinável que você queira mostrar no trajeto especificado.

Você vai chamar a função composable() uma vez para cada uma das quatro rotas.

  1. Chame a função composable(), transmitindo CupcakeScreen.Start.name para a route.
import androidx.navigation.compose.composable

NavHost(
    navController = navController,
    startDestination = CupcakeScreen.Start.name,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {

    }
}
  1. Na lambda final, chame o elemento combinável StartOrderScreen, transmitindo quantityOptions para a propriedade quantityOptions. Para o modifier transfira Modifier.fillMaxSize().padding(dimensionResource(R.dimen.padding_medium))
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.res.dimensionResource
import com.example.cupcake.ui.StartOrderScreen
import com.example.cupcake.data.DataSource

NavHost(
    navController = navController,
    startDestination = CupcakeScreen.Start.name,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {
        StartOrderScreen(
            quantityOptions = DataSource.quantityOptions,
            modifier = Modifier
                .fillMaxSize()
                .padding(dimensionResource(R.dimen.padding_medium))
        )
    }
}
  1. Abaixo da primeira chamada para composable(), chame composable() de novo, transmitindo CupcakeScreen.Flavor.name para a route.
composable(route = CupcakeScreen.Flavor.name) {

}
  1. Na lambda final, acesse uma referência ao LocalContext.current e a armazene em uma variável com o nome context. Context é uma classe abstrata cuja implementação é fornecida pelo sistema Android. Ela permite acesso a recursos e classes específicos do aplicativo, bem como chamadas para operações no aplicativo, como inicialização de atividades etc. Você pode usar essa variável para acessar as strings da lista de IDs de recursos no modelo de visualização que será mostrado na lista de sabores.
import androidx.compose.ui.platform.LocalContext

composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
}
  1. Chame o elemento combinável SelectOptionScreen.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(

    )
}
  1. A tela de sabor precisa mostrar e atualizar o subtotal quando o usuário selecionar uma opção. Transmita uiState.price ao parâmetro subtotal.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price
    )
}
  1. A tela de sabor mostra a lista de sabores dos recursos de string do app. Transforme a lista de IDs de recursos em uma lista de strings usando a função map() e chamando context.resources.getString(id) para cada sabor.
import com.example.cupcake.ui.SelectOptionScreen

composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = DataSource.flavors.map { id -> context.resources.getString(id) }
    )
}
  1. Para o parâmetro onSelectionChanged, transmita uma expressão lambda que chame setFlavor() no modelo de visualização, transmitindo it, que é o argumento transmitido para onSelectionChanged(). Para o parâmetro modifier, transmita Modifier.fillMaxHeight()..
import androidx.compose.foundation.layout.fillMaxHeight
import com.example.cupcake.data.DataSource.flavors

composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = DataSource.flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) },
        modifier = Modifier.fillMaxHeight()
    )
}

A tela de data de retirada é semelhante à tela de sabor. A única diferença são os dados transmitidos ao elemento combinável SelectOptionScreen.

  1. Chame a função composable() novamente, transmitindo CupcakeScreen.Pickup.name ao parâmetro route.
composable(route = CupcakeScreen.Pickup.name) {

}
  1. Na lambda final, chame o elemento combinável SelectOptionScreen e transmita uiState.price ao subtotal, como antes. Transmita uiState.pickupOptions ao parâmetro options e uma expressão lambda que chame setDate() no viewModel para o parâmetro onSelectionChanged. Para o parâmetro modifier, transmita Modifier.fillMaxHeight()..
SelectOptionScreen(
    subtotal = uiState.price,
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) },
    modifier = Modifier.fillMaxHeight()
)
  1. Chame composable() mais uma vez, transmitindo CupcakeScreen.Summary.name para a route.
composable(route = CupcakeScreen.Summary.name) {

}
  1. Na lambda final, chame o elemento combinável OrderSummaryScreen(), transmitindo a variável uiState ao parâmetro orderUiState. Para o parâmetro modifier, transmita Modifier.fillMaxHeight()..
import com.example.cupcake.ui.OrderSummaryScreen

composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState,
        modifier = Modifier.fillMaxHeight()
    )
}

Agora o NavHost está configurado. Na próxima seção, você vai fazer o app mudar as rotas e navegar entre as telas quando o usuário tocar em cada um dos botões.

5. Navegar entre rotas

Agora que você definiu suas rotas e as mapeou para elementos combináveis em um NavHost, é hora de navegar entre as telas. O NavHostController, a propriedade navController vinda de chamar rememberNavController(), é responsável pela navegação entre as rotas. No entanto, essa propriedade é definida no elemento combinável CupcakeApp. É necessário ter uma maneira de a acessar nas diferentes telas do app.

Fácil, não é? Basta transmitir navController como um parâmetro para cada elemento combinável.

Embora essa abordagem funcione, não é a arquitetura ideal para seu app. Um benefício de usar o NavHost para processar a navegação é que a lógica dela é mantida separada da IU individual. Essa opção evita algumas das principais desvantagens de transmitir o navController como um parâmetro.

  • A lógica de navegação é mantida em um só lugar, o que pode facilitar a manutenção do código e impedir bugs, porque as telas individuais não perdem acidentalmente a navegação no app.
  • Em apps que precisam funcionar em diferentes formatos, como smartphones no modo retrato, smartphones dobráveis ou tablets de tela grande, um botão pode ou não acionar a navegação, dependendo do layout do app. As telas individuais precisam ser autônomas e não precisam estar cientes de outras telas no app.

Em vez disso, nossa abordagem é transmitir um tipo de função em cada função de composição quando o usuário clicar no botão. Dessa forma, o elemento combinável e todos os elementos filhos dele decidem quando chamar a função. No entanto, a lógica de navegação não é exposta a telas individuais no app. Todo o comportamento dela é processado no NavHost.

Adicionar gerenciadores de botões a StartOrderScreen

Para começar, adicione um parâmetro de tipo de função que é chamado quando um dos botões de quantidade é pressionado na primeira tela. Essa função é transmitida ao elemento combinável StartOrderScreen e é responsável por atualizar o modelo de visualização e navegar até a próxima tela.

  1. Abra StartOrderScreen.kt.
  2. Abaixo do parâmetro quantityOptions e antes do parâmetro modificador, adicione um parâmetro chamado onNextButtonClicked do tipo () -> Unit.
@Composable
fun StartOrderScreen(
    quantityOptions: List<Pair<Int, Int>>,
    onNextButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Agora que o elemento combinável StartOrderScreen espera um valor para onNextButtonClicked, encontre o StartOrderPreview e transmita um corpo de lambda vazio ao parâmetro onNextButtonClicked.
@Preview
@Composable
fun StartOrderPreview() {
    CupcakeTheme {
        StartOrderScreen(
            quantityOptions = DataSource.quantityOptions,
            onNextButtonClicked = {},
            modifier = Modifier
                .fillMaxSize()
                .padding(dimensionResource(R.dimen.padding_medium))
        )
    }
}

Cada botão corresponde a uma quantidade diferente de cupcakes. Você vai precisar dessas informações para que a função transmitida para onNextButtonClicked possa atualizar o modelo de visualização de acordo com elas.

  1. Modifique o tipo do parâmetro onNextButtonClicked para usar um parâmetro Int.
onNextButtonClicked: (Int) -> Unit,

Para que o Int transmita ao chamar onNextButtonClicked(), veja o tipo de parâmetro quantityOptions.

O tipo é List<Pair<Int, Int>> ou uma lista de Pair<Int, Int>. Talvez o tipo Pair (link em inglês) seja novidade para você, mas é como um nome sugere: um par de valores. Pair usa dois parâmetros de tipo genérico. Nesse caso, ambos são do tipo Int.

8326701a77706258.png

Cada item em um par é acessado pela primeira ou pela segunda propriedade. No caso do parâmetro quantityOptions do elemento combinável StartOrderScreen, o primeiro Int é um ID de recurso para a string ser mostrada em cada botão. O segundo Int é a quantidade real de cupcakes.

Vamos transmitir a segunda propriedade do par selecionado ao chamar a função onNextButtonClicked().

  1. Encontre a expressão lambda vazia para o parâmetro onClick do SelectQuantityButton.
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = {}
    )
}
  1. Na expressão lambda, chame onNextButtonClicked, transmitindo item.second, que é o número de cupcakes.
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = { onNextButtonClicked(item.second) }
    )
}

Adicionar gerenciadores de botões a SelectOptionScreen

  1. Abaixo do parâmetro onSelectionChanged do elemento combinável SelectOptionScreen em SelectOptionScreen.kt, adicione um parâmetro chamado onCancelButtonClicked do tipo () -> Unit com um valor padrão de {}.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Abaixo do parâmetro onCancelButtonClicked, adicione outro parâmetro do tipo () -> Unit, chamado onNextButtonClicked, com um valor padrão de {}.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    onNextButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Transmita onCancelButtonClicked ao parâmetro onClick do botão de cancelamento.
OutlinedButton(
    modifier = Modifier.weight(1f),
    onClick = onCancelButtonClicked
) {
    Text(stringResource(R.string.cancel))
}
  1. Transmita onNextButtonClicked ao parâmetro onClick do botão "Next".
Button(
    modifier = Modifier.weight(1f),
    enabled = selectedValue.isNotEmpty(),
    onClick = onNextButtonClicked
) {
    Text(stringResource(R.string.next))
}

Adicionar gerenciadores de botões à SummaryScreen

Por fim, adicione funções de gerenciador de botões para os botões Cancel e Send na tela de resumo.

  1. No elemento combinável OrderSummaryScreen em SummaryScreen.kt, adicione um parâmetro com o nome onCancelButtonClicked do tipo () -> Unit.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Adicione outro parâmetro do tipo (String, String) -> Unit e dê a ele o nome onSendButtonClicked.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    onSendButtonClicked: (String, String) -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. O elemento combinável OrderSummaryScreen agora espera valores para onSendButtonClicked e onCancelButtonClicked. Encontre o OrderSummaryPreview e transmita um corpo lambda vazio com dois parâmetros String para onSendButtonClicked e um corpo lambda vazio para os parâmetros onCancelButtonClicked.
@Preview
@Composable
fun OrderSummaryPreview() {
   CupcakeTheme {
       OrderSummaryScreen(
           orderUiState = OrderUiState(0, "Test", "Test", "$300.00"),
           onSendButtonClicked = { subject: String, summary: String -> },
           onCancelButtonClicked = {},
           modifier = Modifier.fillMaxHeight()
       )
   }
}
  1. Transmita onSendButtonClicked ao parâmetro onClick do botão Send. Transmita newOrder e orderSummary, as duas variáveis definidas anteriormente em OrderSummaryScreen. Essas strings consistem nos dados reais que o usuário pode compartilhar com outro app.
Button(
    modifier = Modifier.fillMaxWidth(),
    onClick = { onSendButtonClicked(newOrder, orderSummary) }
) {
    Text(stringResource(R.string.send))
}
  1. Transmita onCancelButtonClicked ao parâmetro onClick do botão Cancel.
OutlinedButton(
    modifier = Modifier.fillMaxWidth(),
    onClick = onCancelButtonClicked
) {
    Text(stringResource(R.string.cancel))
}

Para navegar para outra rota, basta chamar o método navigate() na instância de NavHostController.

fc8aae3911a6a25d.png

O método de navegação usa um único parâmetro: uma String correspondente a uma rota definida no NavHost. Se a rota corresponder a uma das chamadas para composable() no NavHost, o app vai navegar para essa tela.

Você vai transmitir funções que chamam navigate() quando o usuário pressiona os botões nas telas Start, Flavor e Pickup.

  1. No CupcakeScreen.kt, localize a chamada para composable() na tela inicial. Transmita uma expressão lambda para o parâmetro onNextButtonClicked.
StartOrderScreen(
    quantityOptions = DataSource.quantityOptions,
    onNextButtonClicked = {
    }
)

Você se lembra da propriedade Int transmitida a essa função para o número de cupcakes? Antes de navegar para a próxima tela, atualize o modelo de visualização para que o app mostre o subtotal correto.

  1. Chame setQuantity no viewModel, transmitindo it.
onNextButtonClicked = {
    viewModel.setQuantity(it)
}
  1. Chame navigate() no navController, transmitindo CupcakeScreen.Flavor.name para a route.
onNextButtonClicked = {
    viewModel.setQuantity(it)
    navController.navigate(CupcakeScreen.Flavor.name)
}
  1. Para o parâmetro onNextButtonClicked na tela de sabor, basta transmitir uma lambda que chame navigate(), transmitindo CupcakeScreen.Pickup.name para a route.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
        options = DataSource.flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
  1. Transmita uma lambda vazia a onCancelButtonClicked, que você vai implementar em seguida.
SelectOptionScreen(
    subtotal = uiState.price,
    onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
    onCancelButtonClicked = {},
    options = DataSource.flavors.map { id -> context.resources.getString(id) },
    onSelectionChanged = { viewModel.setFlavor(it) },
    modifier = Modifier.fillMaxHeight()
)
  1. Para o parâmetro onNextButtonClicked na tela de retirada, transmita uma lambda que chame navigate(), transmitindo CupcakeScreen.Summary.name para a route.
composable(route = CupcakeScreen.Pickup.name) {
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
        options = uiState.pickupOptions,
        onSelectionChanged = { viewModel.setDate(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
  1. Novamente, transmita uma lambda vazia a onCancelButtonClicked().
SelectOptionScreen(
    subtotal = uiState.price,
    onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
    onCancelButtonClicked = {},
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) },
    modifier = Modifier.fillMaxHeight()
)
  1. Para o OrderSummaryScreen, transmita as lambdas vazias do onCancelButtonClicked e do onSendButtonClicked. Adicione os parâmetros para o subject e o summary transmitidos ao elemento onSendButtonClicked, que vão ser implementados em breve.
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState,
        onCancelButtonClicked = {},
        onSendButtonClicked = { subject: String, summary: String ->

        },
        modifier = Modifier.fillMaxHeight()
    )
}

Agora, é possível navegar em cada tela do app. Chamar navigate() não apenas muda a tela, como também a coloca na parte de cima da backstack. Além disso, ao pressionar o botão "Voltar" do sistema, você pode retornar à tela anterior.

O app empilha cada tela acima da anterior, e o botão "Voltar" (bade5f3ecb71e4a2.png) pode removê-las. O histórico de telas, desde startDestination na parte de baixo até a tela mostrada na camada mais acima, é conhecido como backstack.

Ir para a tela inicial

Diferente do botão "Voltar" do sistema, o botão Cancel não volta à tela anterior. Em vez disso, todas as telas da backstack são abertas e removidas, e a tela inicial é retornada.

Para fazer isso, chame o método popBackStack().

2f382e5eb319b4b8.png

O método popBackStack() tem dois parâmetros obrigatórios.

  • route: string que representa a rota do destino para onde você quer navegar de volta.
  • inclusive: um valor booleano que, se for verdadeiro, também vai destacar (remover) a rota especificada. Se for falso, popBackStack() vai remover todos os destinos acima do destino inicial, mas não sem o incluir, deixando-o como a tela na camada superior visível para o usuário.

Quando o usuário pressiona o botão Cancel em qualquer uma das telas, o app redefine o estado no modelo de visualização e chama popBackStack(). Primeiro, você vai implementar um método para fazer isso e, em seguida, transmiti-lo ao parâmetro apropriado nas três telas com os botões Cancel.

  1. Após a função CupcakeApp(), defina uma função particular com o nome cancelOrderAndNavigateToStart().
private fun cancelOrderAndNavigateToStart() {
}
  1. Adicione dois parâmetros: viewModel do tipo OrderViewModel e navController do tipo NavHostController.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
}
  1. No corpo da função, chame resetOrder() no viewModel.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
}
  1. Chame popBackStack() no navController, transmitindo CupcakeScreen.Start.name para a route e false para inclusive.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
    navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
  1. No elemento combinável CupcakeApp(), transmita cancelOrderAndNavigateToStart para os parâmetros onCancelButtonClicked dos dois elementos SelectOptionScreen e o OrderSummaryScreen.
composable(route = CupcakeScreen.Start.name) {
    StartOrderScreen(
        quantityOptions = DataSource.quantityOptions,
        onNextButtonClicked = {
            viewModel.setQuantity(it)
            navController.navigate(CupcakeScreen.Flavor.name)
        },
        modifier = Modifier
            .fillMaxSize()
            .padding(dimensionResource(R.dimen.padding_medium))
    )
}
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
        onCancelButtonClicked = {
            cancelOrderAndNavigateToStart(viewModel, navController)
        },
        options = DataSource.flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
composable(route = CupcakeScreen.Pickup.name) {
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
        onCancelButtonClicked = {
            cancelOrderAndNavigateToStart(viewModel, navController)
        },
        options = uiState.pickupOptions,
        onSelectionChanged = { viewModel.setDate(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState,
        onCancelButtonClicked = {
            cancelOrderAndNavigateToStart(viewModel, navController)
        },
        onSendButtonClicked = { subject: String, summary: String ->

        },
        modifier = Modifier.fillMaxHeight()
   )
}
  1. Execute o app e teste se, ao pressionar o botão Cancel em qualquer uma das telas, o usuário é levado de volta para a primeira tela.

6. Navegar para outro app

Até agora, você aprendeu a navegar para uma tela diferente no app e voltar para a tela inicial. Há apenas mais uma etapa para implementar a navegação no app Cupcake. Na tela de resumo do pedido, o usuário pode enviar o pedido para outro app. Essa seleção mostra uma ShareSheet, um componente da interface do usuário que cobre a parte de baixo da tela e que mostra as opções de compartilhamento.

Essa parte da interface não faz parte do app Cupcake. Na verdade, ela é fornecida pelo sistema operacional Android. A interface do sistema, como a tela de compartilhamento, não é chamada pelo navController. Em vez disso, use algo chamado Intent.

Intent é uma solicitação para que o sistema realize alguma ação, normalmente apresentando uma nova atividade. Há muitas intents diferentes, e é recomendável consultar a documentação para ter uma lista abrangente. No entanto, temos interesse na intent chamada ACTION_SEND. Você pode fornecer essa intent com alguns dados, como uma string, e apresentar ações de compartilhamento apropriadas para esses dados.

O processo básico para configurar uma intent é o seguinte:

  1. Crie um objeto da intent e a especifique, como ACTION_SEND.
  2. Especifique o tipo de dados adicionais enviados com a intent. Para um texto simples, você pode usar "text/plain", mas há outros tipos disponíveis, como "image/*" ou "video/*".
  3. Transmita quaisquer dados adicionais para a intent, como a imagem ou o texto a ser compartilhado, chamando o método putExtra(). Essa intent vai precisar de dois extras: EXTRA_SUBJECT e EXTRA_TEXT.
  4. Chame o método de contexto startActivity(), transmitindo uma atividade criada a partir da intent.

Vamos mostrar como criar uma intent de ação de compartilhamento, mas o processo é o mesmo para outros tipos de intents. Para projetos futuros, consulte a documentação conforme necessário para ver o tipo específico de dados e os extras necessários.

Conclua as etapas a seguir para criar uma intent e enviar o pedido de cupcake a outro app:

  1. Em CupcakeScreen.kt, abaixo do elemento combinável CupcakeApp, crie uma função particular com o nome shareOrder().
private fun shareOrder()
  1. Adicione um parâmetro chamado context do tipo Context.
import android.content.Context

private fun shareOrder(context: Context) {
}
  1. Adicione dois parâmetros String: subject e summary. Essas strings vão ser mostradas na página de ações de compartilhamento.
private fun shareOrder(context: Context, subject: String, summary: String) {
}
  1. No corpo da função, crie uma intent com o nome intent e transmita Intent.ACTION_SEND como um argumento.
import android.content.Intent

val intent = Intent(Intent.ACTION_SEND)

Como você só precisa configurar esse objeto Intent uma vez, pode tornar as próximas linhas de código mais concisas usando a função apply(), que você aprendeu em um codelab anterior.

  1. Chame apply() na intent recém-criada e transmita uma expressão lambda.
val intent = Intent(Intent.ACTION_SEND).apply {

}
  1. No corpo da lambda, defina o tipo como "text/plain". Como você está fazendo isso em uma função transmitida para apply(), não é necessário referenciar o identificador do objeto, intent.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
}
  1. Chame putExtra(), transmitindo o assunto de EXTRA_SUBJECT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
}
  1. Chame putExtra(), transmitindo o resumo de EXTRA_TEXT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
    putExtra(Intent.EXTRA_TEXT, summary)
}
  1. Chame o método de contexto startActivity().
context.startActivity(

)
  1. Na lambda transmitida para startActivity(), crie uma atividade da intent chamando o método de classe createChooser(). Transmita a intent do primeiro argumento e do recurso de string new_cupcake_order.
context.startActivity(
    Intent.createChooser(
        intent,
        context.getString(R.string.new_cupcake_order)
    )
)
  1. No elemento combinável CupcakeApp, na chamada de composable() para o CucpakeScreen.Summary.name, acesse uma referência ao objeto de contexto para que ele possa ser transmitido à função shareOrder().
composable(route = CupcakeScreen.Summary.name) {
    val context = LocalContext.current

    ...
}
  1. No corpo da lambda de onSendButtonClicked(), chame shareOrder(), transmitindo context, subject e summary como argumentos.
onSendButtonClicked = { subject: String, summary: String ->
    shareOrder(context, subject = subject, summary = summary)
}
  1. Execute o app e navegue pelas telas.

Ao clicar em Send Order to Another App (enviar pedido a outro app), você vai encontrar ações de compartilhamento como Mensagem e Bluetooth na página inferior, junto com o assunto e o resumo fornecidos como extras.

a32e016a6ccbf427.png

7. Fazer a barra de apps responder à navegação

Embora o app funcione e possa navegar entre as telas, ainda falta algo que está nas capturas de tela no início deste codelab. A barra de apps não responde automaticamente à navegação. O título não é atualizado quando o app navega para uma nova rota nem mostra o botão "Up" antes do título, quando apropriado.

O código inicial inclui um elemento combinável para gerenciar a AppBar chamado CupcakeAppBar. Agora que você implementou a navegação no app, pode usar as informações da backstack para mostrar o título correto e o botão "UP", se apropriado. O elemento combinável CupcakeAppBar precisa estar ciente da tela atual para que o título seja atualizado corretamente.

  1. No tipo enumerado CupcakeScreen em CupcakeScreen.kt, adicione um parâmetro do tipo Int com o nome title usando a anotação @StringRes.
import androidx.annotation.StringRes

enum class CupcakeScreen(@StringRes val title: Int) {
    Start,
    Flavor,
    Pickup,
    Summary
}
  1. Adicione um valor de recurso para cada caso de tipo enumerado, correspondendo ao texto do título de cada tela. Use app_name para a tela Start, choose_flavor para a tela Flavor, choose_pickup_date para a tela Pickup e order_summary para a tela Summary.
enum class CupcakeScreen(@StringRes val title: Int) {
    Start(title = R.string.app_name),
    Flavor(title = R.string.choose_flavor),
    Pickup(title = R.string.choose_pickup_date),
    Summary(title = R.string.order_summary)
}
  1. Adicione um parâmetro com o nome currentScreen do tipo CupcakeScreen ao elemento combinável CupcakeAppBar.
fun CupcakeAppBar(
    currentScreen: CupcakeScreen,
    canNavigateBack: Boolean,
    navigateUp: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Dentro de CupcakeAppBar, substitua o nome codificado do app pelo título da tela atual, transmitindo currentScreen.title à chamada de stringResource() para o parâmetro de título de TopAppBar.
TopAppBar(
    title = { Text(stringResource(currentScreen.title)) },
    modifier = modifier,
    navigationIcon = {
        if (canNavigateBack) {
            IconButton(onClick = navigateUp) {
                Icon(
                    imageVector = Icons.Filled.ArrowBack,
                    contentDescription = stringResource(R.string.back_button)
                )
            }
        }
    }
)

O botão "Up" só vai aparecer se houver um elemento combinável na backstack. Se o app não tiver telas na backstack (StartOrderScreen aparece), o botão "Up" não será mostrado. Para verificar isso, você precisa de uma referência à backstack.

  1. No elemento combinável CupcakeApp, abaixo da variável navController, crie uma variável com o nome backStackEntry e chame o método currentBackStackEntryAsState() do navController usando o delegado by.
import androidx.navigation.compose.currentBackStackEntryAsState

@Composable
fun CupcakeApp(
    viewModel: OrderViewModel = viewModel(),
    navController: NavHostController = rememberNavController()
){

    val backStackEntry by navController.currentBackStackEntryAsState()

    ...
}
  1. Converta o título da tela atual em um valor de CupcakeScreen. Abaixo da variável backStackEntry, crie uma variável usando val com o nome currentScreen igual ao resultado da chamada da função de classe valueOf() de CupcakeScreen e transmita a rota do destino de backStackEntry. Use o operador Elvis para fornecer um valor padrão de CupcakeScreen.Start.name.
val currentScreen = CupcakeScreen.valueOf(
    backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
  1. Transmita o valor da variável currentScreen para o parâmetro com o mesmo nome do elemento combinável CupcakeAppBar.
CupcakeAppBar(
    currentScreen = currentScreen,
    canNavigateBack = false,
    navigateUp = {}
)

Se houver uma tela atrás da tela atual na backstack, o botão "Up" vai aparecer. Você pode usar uma expressão booleana para identificar se o botão "Up" será mostrado.

  1. Para o parâmetro canNavigateBack, transmita uma expressão booleana verificando se a propriedade previousBackStackEntry de navController não é igual a nulo.
canNavigateBack = navController.previousBackStackEntry != null,
  1. Para voltar à tela anterior, chame o método navigateUp() de navController.
navigateUp = { navController.navigateUp() }
  1. Execute o app.

O título AppBar agora é atualizado para refletir a tela atual. Quando você navega para uma tela diferente de StartOrderScreen, o botão "Up" aparece e leva você de volta à tela anterior.

3fd023516061f522.gif

8. 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-cupcake.git
$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout navigation

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

Confira o código da solução deste codelab no GitHub (link em inglês).

9. Resumo

Parabéns! Você acabou de passar de aplicativos simples de tela única para um app complexo de várias telas usando o componente de navegação do Jetpack para navegar por várias telas. Você definiu as rotas, processou todas elas em um NavHost e usou parâmetros de tipo de função para separar a lógica de navegação das telas individuais. Você também aprendeu a enviar dados para outro app usando intents e a personalizar a barra de apps em resposta à navegação. Nas próximas unidades, você vai continuar usando essas habilidades ao trabalhar em vários outros apps multitelas de complexidade cada vez maior.

Saiba mais