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
eColumn
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.
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.
- No
CupcakeScreen.kt
, acima do elemento combinávelCupcakeAppBar
, 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 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.
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 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()
.
- Abra o
CupcakeScreen.kt
. - No
Scaffold
, abaixo da variáveluiState
, adicione um elemento combinávelNavHost
.
import androidx.navigation.compose.NavHost
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.
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.
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 combinável 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
.
import androidx.navigation.compose.composable
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
}
}
- Na lambda final, chame o elemento combinável
StartOrderScreen
, transmitindoquantityOptions
para a propriedadequantityOptions
. Para omodifier
transfiraModifier.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))
)
}
}
- Abaixo da primeira chamada para
composable()
, chamecomposable()
de novo, 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
.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
}
- Chame o elemento combinável
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. Transforme a lista de IDs de recursos em uma lista de strings usando a função
map()
e chamandocontext.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) }
)
}
- Para o parâmetro
onSelectionChanged
, transmita uma expressão lambda que chamesetFlavor()
no modelo de visualização, transmitindoit
, que é o argumento transmitido paraonSelectionChanged()
. Para o parâmetromodifier
, transmitaModifier.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
.
- Chame a função
composable()
novamente, transmitindoCupcakeScreen.Pickup.name
ao parâmetroroute
.
composable(route = CupcakeScreen.Pickup.name) {
}
- Na lambda final, chame o elemento combinável
SelectOptionScreen
e transmitauiState.price
aosubtotal
, como antes. TransmitauiState.pickupOptions
ao parâmetrooptions
e uma expressão lambda que chamesetDate()
noviewModel
para o parâmetroonSelectionChanged
. Para o parâmetromodifier
, transmitaModifier.fillMaxHeight().
.
SelectOptionScreen(
subtotal = uiState.price,
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
- Chame
composable()
mais uma vez, transmitindoCupcakeScreen.Summary.name
para aroute
.
composable(route = CupcakeScreen.Summary.name) {
}
- Na lambda final, chame o elemento combinável
OrderSummaryScreen()
, transmitindo a variáveluiState
ao parâmetroorderUiState
. Para o parâmetromodifier
, transmitaModifier.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.
- 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
){
...
}
- Agora que o elemento combinável
StartOrderScreen
espera um valor paraonNextButtonClicked
, encontre oStartOrderPreview
e transmita um corpo de lambda vazio ao parâmetroonNextButtonClicked
.
@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.
- 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
(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
.
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()
.
- Encontre a expressão lambda vazia 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 combinávelSelectOptionScreen
emSelectOptionScreen.kt
, adicione um parâmetro chamadoonCancelButtonClicked
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
)
- Abaixo do parâmetro
onCancelButtonClicked
, adicione outro parâmetro do tipo() -> Unit
, chamadoonNextButtonClicked
, com um valor padrão de{}
.
@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 combinável
OrderSummaryScreen
emSummaryScreen.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
(String, String) -> Unit
e dê a ele o nomeonSendButtonClicked
.
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
onSendButtonClicked: (String, String) -> Unit,
modifier: Modifier = Modifier
){
...
}
- O elemento combinável
OrderSummaryScreen
agora espera valores paraonSendButtonClicked
eonCancelButtonClicked
. Encontre oOrderSummaryPreview
e transmita um corpo lambda vazio com dois parâmetrosString
paraonSendButtonClicked
e um corpo lambda vazio para os parâmetrosonCancelButtonClicked
.
@Preview
@Composable
fun OrderSummaryPreview() {
CupcakeTheme {
OrderSummaryScreen(
orderUiState = OrderUiState(0, "Test", "Test", "$300.00"),
onSendButtonClicked = { subject: String, summary: String -> },
onCancelButtonClicked = {},
modifier = Modifier.fillMaxHeight()
)
}
}
- 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 = 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.
- 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 sabor, 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 = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
- 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()
)
- 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) },
modifier = Modifier.fillMaxHeight()
)
}
- 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()
)
- Para o
OrderSummaryScreen
, transmita as lambdas vazias doonCancelButtonClicked
e doonSendButtonClicked
. Adicione os parâmetros para osubject
e osummary
transmitidos ao elementoonSendButtonClicked
, 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" () 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()
.
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 combinável
CupcakeApp()
, transmitacancelOrderAndNavigateToStart
para os parâmetrosonCancelButtonClicked
dos dois elementosSelectOptionScreen
e oOrderSummaryScreen
.
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()
)
}
- 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:
- 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 combinável
CupcakeApp
, crie uma função particular com o nomeshareOrder()
.
private fun shareOrder()
- Adicione um parâmetro chamado
context
do tipoContext
.
import android.content.Context
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.
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.
- 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 combinável
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 (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.
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.
- No tipo enumerado
CupcakeScreen
em CupcakeScreen.kt, adicione um parâmetro do tipoInt
com o nometitle
usando a anotação@StringRes
.
import androidx.annotation.StringRes
enum class CupcakeScreen(@StringRes val title: Int) {
Start,
Flavor,
Pickup,
Summary
}
- 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 telaStart
,choose_flavor
para a telaFlavor
,choose_pickup_date
para a telaPickup
eorder_summary
para a telaSummary
.
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)
}
- Adicione um parâmetro com o nome
currentScreen
do tipoCupcakeScreen
ao elemento combinávelCupcakeAppBar
.
fun CupcakeAppBar(
currentScreen: CupcakeScreen,
canNavigateBack: Boolean,
navigateUp: () -> Unit = {},
modifier: Modifier = Modifier
)
- Dentro de
CupcakeAppBar
, substitua o nome codificado do app pelo título da tela atual, transmitindocurrentScreen.title
à chamada destringResource()
para o parâmetro de título deTopAppBar
.
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.
- No elemento combinável
CupcakeApp
, abaixo da variávelnavController
, crie uma variável com o nomebackStackEntry
e chame o métodocurrentBackStackEntryAsState()
donavController
usando o delegadoby
.
import androidx.navigation.compose.currentBackStackEntryAsState
@Composable
fun CupcakeApp(
viewModel: OrderViewModel = viewModel(),
navController: NavHostController = rememberNavController()
){
val backStackEntry by navController.currentBackStackEntryAsState()
...
}
- Converta o título da tela atual em um valor de
CupcakeScreen
. Abaixo da variávelbackStackEntry
, crie uma variável usandoval
com o nomecurrentScreen
igual ao resultado da chamada da função de classevalueOf()
deCupcakeScreen
e transmita a rota do destino debackStackEntry
. Use o operador Elvis para fornecer um valor padrão deCupcakeScreen.Start.name
.
val currentScreen = CupcakeScreen.valueOf(
backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
- Transmita o valor da variável
currentScreen
para o parâmetro com o mesmo nome do elemento combinávelCupcakeAppBar
.
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.
- 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 baixar o 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 baixar o 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.