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 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
eColumn
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.
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.
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 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.
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 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.
- No
CupcakeScreen.kt
, acima do elemento de composiçãoCupcakeAppBar
, adicione uma classe de enumeração com o nomeCupcakeScreen
.
enum class CupcakeScreen() {
}
- Adicione quatro casos à classe de enumeração:
Start
,Flavor
,Pickup
eSummary
.
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.
Há dois parâmetros importantes.
navController
: uma instância da classeNavHostController
. É possível usar esse objeto para navegar entre telas, por exemplo, chamando o métodonavigate()
para navegar para outro destino. Você pode buscar oNavHostController
chamandorememberNavController()
em uma função de composição.startDestination
: uma rota de string que define o destino mostrado por padrão quando o app mostra oNavHost
pela primeira vez. No caso do app Cupcake, é a rotaStart
.
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()
.
- Abra
CupcakeScreen.kt
. - Acima da variável
viewModel
no elemento de composiçãoCupcakeApp
, crie uma nova variável usandoval
com o nomenavController
e a defina igual ao resultado da chamada derememberNavController()
.
@Composable
fun CupcakeApp(modifier: Modifier = Modifier){
val navController = rememberNavController()
...
}
- No
Scaffold
, abaixo da variáveluiState
, adicione um elemento de composiçãoNavHost
.
Scaffold(
...
) { innerPadding ->
val uiState by viewModel.uiState.collectAsState()
NavHost()
}
- Transmita a variável
navController
ao parâmetronavController
eCupcakeScreen.Start.name
ao parâmetrostartDestination
. Transmita o modificador que foi transmitido aoCupcakeApp()
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.
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çãoCupcakeScreen
.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.
- Chame a função
composable()
, transmitindoCupcakeScreen.Start.name
para aroute
.
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
}
}
- Na lambda final, chame o elemento de composição
StartOrderScreen
, transmitindoquantityOptions
para a propriedadequantityOptions
.
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
StartOrderScreen(
quantityOptions = quantityOptions
)
}
}
- Abaixo da primeira chamada para
composable()
, chamecomposable()
novamente, transmitindoCupcakeScreen.Flavor.name
para aroute
.
composable(route = CupcakeScreen.Flavor.name) {
}
- Na lambda final, acesse uma referência ao
LocalContext.current
e a armazene em uma variável com o nomecontext
. É 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
}
- Chame o elemento de composição
SelectOptionScreen
.
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
)
}
- A tela de sabor precisa mostrar e atualizar o subtotal quando o usuário selecionar uma opção. Transmita
uiState.price
ao parâmetrosubtotal
.
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price
)
}
- 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 chamandostringResource()
.
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
options = flavors.map { id -> stringResource(id) }
)
}
- Para o parâmetro
onSelectionChanged
, transmita uma expressão lambda que chamesetFlavor()
no modelo de visualização, transmitindoit
, que é o argumento transmitido paraonSelectionChanged()
.
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
.
- Chame a função
composable()
novamente, transmitindoCupcakeScreen.Pickup.name
ao parâmetroroute
.
composable(route = CupcakeScreen.Pickup.name) {
}
- Na lambda final, chame o elemento de composição
SelectOptionScreen
e transmitauiState.price
aosubtotal
, como antes. TransmitauiState.pickupOptions
ao parâmetrooptions
e uma expressão lambda que chamesetDate()
noviewModel
para o parâmetroonSelectionChanged
.
SelectOptionScreen(
subtotal = uiState.price,
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) }
)
- Chame
composable()
mais uma vez, transmitindoCupcakeScreen.Summary.name
para aroute
.
composable(route = CupcakeScreen.Summary.name) {
}
- Na lambda final, chame o elemento de composição
OrderSummaryScreen()
, transmitindo a variáveluiState
ao parâmetroorderUiState
.
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.
- Abra
StartOrderScreen.kt
. - Abaixo do parâmetro
quantityOptions
e antes do parâmetro modificador, adicione um parâmetro chamadoonNextButtonClicked
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.
- Modifique o tipo do parâmetro
onNextButtonClicked
para usar um parâmetroInt
.
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
.
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()
.
- Transmita uma expressão lambda para o parâmetro
onClick
doSelectQuantityButton
.
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = { }
)
}
- Na expressão lambda, chame
onNextButtonClicked
, transmitindoitem.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
- Abaixo do parâmetro
onSelectionChanged
do elemento de composiçãoSelectOptionScreen
emSelectOptionScreen.kt
, adicione um parâmetro com o nomeonCancelButtonClicked
e o tipo() -> Unit
.
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
)
- Abaixo do parâmetro
onCancelButtonClicked
, adicione outro parâmetro do tipo() -> Unit
chamadoonNextButtonClicked
.
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
onNextButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
)
- Transmita
onCancelButtonClicked
ao parâmetroonClick
do botão de cancelamento.
OutlinedButton(modifier = Modifier.weight(1f), onClick = onCancelButtonClicked) {
Text(stringResource(R.string.cancel))
}
- Transmita
onNextButtonClicked
ao parâmetroonClick
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.
- No elemento de composição
OrderSummaryScreen
emOrderSummaryScreen.kt
, adicione um parâmetro com o nomeonCancelButtonClicked
do tipo() -> Unit
.
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
modifier: Modifier = Modifier
){
...
}
- Adicione outro parâmetro do tipo
() -> Unit
e dê a ele o nomeonSendButtonClicked
.
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
onSendButtonClicked: (String, String) -> Unit,
modifier: Modifier = Modifier
){
...
}
- Transmita
onSendButtonClicked
ao parâmetroonClick
do botão Send. TransmitanewOrder
eorderSummary
, as duas variáveis definidas anteriormente emOrderSummaryScreen
. 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))
}
- Transmita
onCancelButtonClicked
ao parâmetroonClick
do botão Cancel.
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
Navegar para outra rota
Para navegar para outra rota, basta chamar o método navigate()
na instância de NavHostController
.
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
.
- No
CupcakeScreen.kt
, localize a chamada paracomposable()
na tela inicial. Transmita uma expressão lambda para o parâmetroonNextButtonClicked
.
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.
- Chame
setQuantity
noviewModel
, transmitindoit
.
onNextButtonClicked = {
viewModel.setQuantity(it)
}
- Chame
navigate()
nonavController
, transmitindoCupcakeScreen.Flavor.name
para aroute
.
onNextButtonClicked = {
viewModel.setQuantity(it)
navController.navigate(CupcakeScreen.Flavor.name)
}
- Para o parâmetro
onNextButtonClicked
na tela de variação, basta transmitir uma lambda que chamenavigate()
, transmitindoCupcakeScreen.Pickup.name
para aroute
.
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) }
)
}
- 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) }
)
- Para o parâmetro
onNextButtonClicked
na tela de retirada, transmita uma lambda que chamenavigate()
, transmitindoCupcakeScreen.Summary.name
para aroute
.
composable(route = CupcakeScreen.Pickup.name) {
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = {
navController.navigate(CupcakeScreen.Summary.name)
},
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) }
)
}
- 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) }
)
- Para o
OrderSummaryScreen
, transmita as lambdas vazias doonCancelButtonClicked
e doonSendButtonClicked
. Adicione os parâmetros parasubject
esummary
transmitidos aonSendButtonClicked
, 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" () 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()
.
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.
- Após a função
CupcakeApp()
, defina uma função particular com o nomecancelOrderAndNavigateToStart()
.
private fun cancelOrderAndNavigateToStart() {
}
- Adicione dois parâmetros:
viewModel
do tipoOrderViewModel
enavController
do tipoNavHostController
.
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
}
- No corpo da função, chame
resetOrder()
noviewModel
.
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
}
- Chame
popBackStack()
nonavController
, transmitindoCupcakeScreen.Start.name
para aroute
efalse
parainclusive
.
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
- No elemento de composição
CupcakeApp()
, transmitacancelOrderAndNavigateToStart
para os parâmetrosonCancelButtonClicked
dos dois elementosSelectOptionScreen
e oOrderSummaryScreen
.
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) }
)
}
- 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:
- Crie um objeto da intent e a especifique, como
ACTION_SEND
. - 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/*"
. - 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
eEXTRA_TEXT
. - 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:
- Em CupcakeScreen.kt, abaixo do elemento de composição
CupcakeApp
, crie uma função particular com o nomeshareOrder()
.
private fun shareOrder()
- Adicione um parâmetro chamado
context
do tipoContext
.
private fun shareOrder(context: Context) {
}
- Adicione dois parâmetros
String
:subject
esummary
. Essas strings vão ser mostradas na página de ações de compartilhamento.
private fun shareOrder(context: Context, subject: String, summary: String) {
}
- No corpo da função, crie uma intent com o nome
intent
e transmitaIntent.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.
- Chame
apply()
na intent recém-criada e transmita uma expressão lambda.
val intent = Intent(Intent.ACTION_SEND).apply {
}
- No corpo da lambda, defina o tipo como
"text/plain"
. Como você está fazendo isso em uma função transmitida paraapply()
, não é necessário referenciar o identificador do objeto,intent
.
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
}
- Chame
putExtra()
, transmitindo o assunto deEXTRA_SUBJECT
.
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
}
- Chame
putExtra()
, transmitindo o resumo deEXTRA_TEXT
.
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, summary)
}
- Chame o método de contexto
startActivity()
.
context.startActivity(
)
- Na lambda transmitida para
startActivity()
, crie uma atividade da intent chamando o método de classecreateChooser()
. Transmita a intent do primeiro argumento e do recurso de stringnew_cupcake_order
.
context.startActivity(
Intent.createChooser(
intent,
context.getString(R.string.new_cupcake_order)
)
)
- No elemento de composição
CupcakeApp
, na chamada decomposable()
para oCucpakeScreen.Summary.name
, acesse uma referência ao objeto de contexto para que ele possa ser transmitido à funçãoshareOrder()
.
composable(route = CupcakeScreen.Summary.name) {
val context = LocalContext.current
...
}
- No corpo da lambda de
onSendButtonClicked()
, chameshareOrder()
, transmitindocontext
,subject
esummary
como argumentos.
onSendButtonClicked = { subject: String, summary: String ->
shareOrder(context, subject = subject, summary = summary)
}
- 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.
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.
- No elemento de composição
CupcakeApp
, abaixo da variávelnavController
, crie uma variável com o nomebackStackEntry
e chame o métodocurrentBackStackEntry()
donavController
usando o delegadoby
.
@Composable
fun CupcakeApp(modifier: Modifier = Modifier, viewModel: OrderViewModel = viewModel()){
val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState()
...
}
- Na
CupcakeAppBar
, transmitabackStackEntry?.destination?.route
para o parâmetrocurrentScreen
. Como ele é anulável, use o operador elvis (?:
) para especificarCupcakeScreen.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.
- Para o parâmetro
canNavigateBack
, transmita uma expressão booleana verificando se a propriedadepreviousBackStackEntry
denavController
não é igual a nulo.
canNavigateBack = navController.previousBackStackEntry != null,
- Para voltar à tela anterior, chame o método
navigateUp()
denavController
.
navigateUp = { navController.navigateUp() }
- 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.
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.