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.

Primeira página do app Configurações do Android.

Página das Configurações depois que o usuário seleciona "Dispositivos conectados" na primeira página.

Página das Configurações depois que o usuário seleciona "Parear um novo dispositivo" na página anterior.

No Modern Android Development, apps multitelas são criados usando o componente de navegação do Jetpack. O componente de navegação 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 de navegação 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ê vai aprender

  • Criar um elemento de composição 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

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.

Tela de início do pedido

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

Primeira tela do app Cupcake com opções para iniciar um pedido de um, seis ou doze cupcakes.

No código, isso é representado pelo elemento de composição 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.

App Cupcake com opções de sabores diferentes.

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.

App Cupcake com opções de datas de retirada.

As telas Choose Flavor e Choose Pickup Date são representadas pelo mesmo elemento de composição, SelectOptionScreen em SelectOptionScreen.kt. Por que usar o mesmo elemento de composição? O layout dessas telas é exatamente o mesmo. A única diferença são os dados, mas você pode usar o mesmo elemento de composição 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, em que o usuário pode analisar e concluir o pedido.

O app Cupcake mostrando o resumo do pedido, com a quantidade, o sabor, a data de retirada e o subtotal, além de opções para enviar o pedido para outro app ou cancelar.

Essa tela é implementada pelo elemento de composição OrderSummaryScreen em OrderSummaryScreen.kt.

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

Se o usuário optar por enviar o pedido para outro app, o app Cupcake vai abrir uma página inferior que mostra diferentes opções de compartilhamento.

O app Cupcake apresentando ao usuário opções de compartilhamento, como SMS ou e-mail.

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 de composição 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 de composição 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 de composição 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 de composição 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 de composição RadioButton e um elemento Text que contém cada string. Uma Column envolve todo o layout e também contém um elemento de composição 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 de composição 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 de composição 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 de composição que mostra outros destinos de composição, 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 de composição.

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 de composição, 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 de composição CupcakeApp().

  1. Abra CupcakeScreen.kt.
  2. Acima da variável viewModel no elemento de composição CupcakeApp, crie uma nova variável usando val com o nome navController e a defina igual ao resultado da chamada de rememberNavController().
@Composable
fun CupcakeApp(modifier: Modifier = Modifier){
    val navController = rememberNavController()

    ...
}
  1. No Scaffold, abaixo da variável uiState, adicione um elemento de composição 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.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
}

Processar rotas no NavHost

Como outros elementos de composição, 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 de composição 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.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {

    }
}
  1. Na lambda final, chame o elemento de composição StartOrderScreen, transmitindo quantityOptions para a propriedade quantityOptions.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {
        StartOrderScreen(
            quantityOptions = quantityOptions
        )
    }
}
  1. Abaixo da primeira chamada para composable(), chame composable() novamente, 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. É possível usar essa variável para extrair as strings da lista de IDs de recursos no modelo de visualização, com o objetivo de mostrar a lista de sabores.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current

}
  1. Chame o elemento de composição 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. Crie uma lista de strings a partir da lista de sabores no modelo de visualização. Você pode transformar a lista de IDs de recursos em uma lista de strings usando a função map() e chamando stringResource().
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> stringResource(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().
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}

A tela de data de retirada é semelhante à tela de sabor. A única diferença são os dados transmitidos ao elemento de composição 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 de composição 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.
SelectOptionScreen(
    subtotal = uiState.price,
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) }
)
  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 de composição OrderSummaryScreen(), transmitindo a variável uiState ao parâmetro orderUiState.
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState
    )
}

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 de composição 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 de composição 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 de composição.

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 de composição 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 de composição 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
){
...
}

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 não seja conhecido por 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 de composição 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. Transmita uma expressão lambda 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 de composição SelectOptionScreen em SelectOptionScreen.kt, adicione um parâmetro com o nome onCancelButtonClicked e o tipo () -> Unit.
@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.
@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 de composição OrderSummaryScreen em OrderSummaryScreen.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 () -> Unit e dê a ele o nome onSendButtonClicked.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    onSendButtonClicked: (String, String) -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  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 = 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 variação, 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 = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}
  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 = flavors.map { id -> context.resources.getString(id) },
    onSelectionChanged = { viewModel.setFlavor(it) }
)
  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) }
    )
}
  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) }
)
  1. Para o OrderSummaryScreen, transmita as lambdas vazias do onCancelButtonClicked e do onSendButtonClicked. Adicione os parâmetros para subject e summary transmitidos a onSendButtonClicked, que vão ser implementados em breve.
composable(route = CupcakeScreen.Summary.name) {
   val context = LocalContext.current
   OrderSummaryScreen(
       orderUiState = uiState,
       onCancelButtonClicked = {},
       onSendButtonClicked = { subject: String, summary: String ->

       }
   )
}

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 superior, é 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 de composição CupcakeApp(), transmita cancelOrderAndNavigateToStart para os parâmetros onCancelButtonClicked dos dois elementos SelectOptionScreen e o OrderSummaryScreen.
composable(route = CupcakeScreen.Start.name) {
   StartOrderScreen(
       quantityOptions = quantityOptions,
       onNextButtonClicked = {
           viewModel.setQuantity(it)
           navController.navigate(CupcakeScreen.Flavor.name)
       }
   )
}
composable(route = CupcakeScreen.Flavor.name) {
   val context = LocalContext.current
   SelectOptionScreen(
       subtotal = uiState.price,
       onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
       onCancelButtonClicked = {
           cancelOrderAndNavigateToStart(viewModel, navController)
       },
       options = flavors.map { id -> context.resources.getString(id) },
       onSelectionChanged = { viewModel.setFlavor(it) }
   )
}
  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 raiz. 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 página inferior, 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 IU não faz parte do app Cupcake. Na verdade, ela é fornecida pelo sistema operacional Android. A IU 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 de composição CupcakeApp, crie uma função particular com o nome shareOrder().
private fun shareOrder()
  1. Adicione um parâmetro chamado context do tipo 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.
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 de composição 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, você vai encontrar ações de compartilhamento como Messaging e Bluetooth na página inferior, junto com o assunto e o resumo fornecidos como extras.

O app Cupcake apresentando ao usuário opções de compartilhamento, como SMS ou e-mail.

7. Fazer a AppBar 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 AppBar 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 de composição 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 botão "Up" só vai aparecer se houver um elemento de composição na backstack. Se o app não tiver telas na backstack (StartOrderScreen aparece), o botão "Up" não vai ser mostrado. Para verificar isso, você precisa de uma referência à backstack.

  1. No elemento de composição CupcakeApp, abaixo da variável navController, crie uma variável com o nome backStackEntry e chame o método currentBackStackEntry() do navController usando o delegado by.
@Composable
fun CupcakeApp(modifier: Modifier = Modifier, viewModel: OrderViewModel = viewModel()){

    val navController = rememberNavController()

    val backStackEntry by navController.currentBackStackEntryAsState()

    ...
}
  1. Na CupcakeAppBar, transmita backStackEntry?.destination?.route para o parâmetro currentScreen. Como ele é anulável, use o operador elvis (?:) para especificar CupcakeScreen.Start.name como o padrão.
currentScreen = backStackEntry?.destination?.route ?: CupcakeScreen.Start.name,

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

  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.

Animação mostrando o usuário navegando por todas as telas no app Cupcake concluído.

8. 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-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