Alura > Cursos de Mobile > Cursos de Android > Conteúdos de Android > Primeiras aulas do curso Jetpack Compose: Navigation com Type Safety

Jetpack Compose: Navigation com Type Safety

Melhorando a navegação com o Type Safety - Apresentação

Boas-vindas ao Curso de Jetpack Compose: Navigation II. Me chamo Alex Felipe e serei o seu instrutor ao longo deste curso.

Alex é um homem negro de pele clara, com cabelos raspados e possui barba rente ao rosto. Seus olhos são castanho-escuros. Está vestindo uma camisa preta lisa, usa brincos pequenos e redondos. Está sentado em uma cadeira gamer preta, com um microfone mais à frente do instrutor. Ao fundo, há uma parede na cor branca com uma luz gradiente azul do lado esquerdo e roxa do lado direito.

Pré-requisitos

Caso você não tenha conhecimento do que é o Navigation, principalmente fazemos isso com Jetpack Compose, é importante que você conclua os cursos sobre esse conteúdo aqui na Alura. Os links desses cursos estão na página inicial deste curso, em "Pré-requisitos".

Vamos visualizar o projeto que usaremos como base, acessível no GitHub do próprio projeto do curso inicial de Navigation.

O nome do projeto é Panucci. A ideia é simular um aplicativo de restaurante, que exibe a lista de produtos, os destaques do dia, as bebidas, o menu principal, a tela de pedidos, de detalhes do produto e assim por diante. Tudo isso é feito no primeiro curso!

A partir deste curso de Type Safety, aprenderemos novas técnicas e também mais uma funcionalidade para esse projeto. Então vamos conhecer essas novidades!

Iniciaremos pela parte do projeto. Se pegarmos o projeto do Panucci, ele vai ter uma abordagem diferente quando acessamos a tela de detalhes do produto.

Por exemplo, a partir da tela "Destaques do dia" foi configurada uma falha na busca desse produto. Vamos verificar como o nosso aplicativo vai reagir.

Ao clicarmos no primeiro produto da tela de destaques, observe que conseguimos visualizar uma tela de carregamento tentando carregar os detalhes do produto (essa será uma das funcionalidades que vamos implementar). Mas recebemos a mensagem "Falha ao buscar o produto", com um botão abaixo "Tentar buscar novamente" e outro "Voltar".

Nas outras telas não temos esse problema (propositalmente), como, por exemplo, na tela de Menu. Clicando em "Menu" na parte inferior da tela e depois no primeiro cartão, observe que exibe somente o carregamento. Aprenderemos como implementar isso dentro do nosso projeto.

Sobre a parte técnica, envolvendo o código, faremos uma introdução do que é Type Safety. Uma técnica bem importante quando trabalhamos com o Navigation utilizando o Jetpack Compose, porque será a partir dele que vamos organizar melhor o código e mais segurança na navegação (seja ela simples, com argumentos, com opções de navegação).

Após configurarmos todo o Type Safety, aprenderemos também como conectamos tudo o que fazemos com o Navigation com os componentes de gerenciamento de estado. Como em why state e view model.

Dessa forma, fechamos o fluxo do nosso aplicativo com uma abordagem comum em qualquer aplicativo com Jetpack Compose.

É um curso importante para quem está aprendendo e quer conhecer cada vez mais o Jetpack Compose. Espero que você tenha gostado da proposta do curso, e conto com a sua presença na primeira aula.

Vamos lá?

Melhorando a navegação com o Type Safety - Problema atual de navegação

A configuração de navegação de telas foi construída a partir dos três principais componentes do Navigation, sendo eles: NavHost, navController e o grafo de navegação.

A partir dos grafos, determinamos os destinos que desejamos configurar do aplicativo para exibir cada tela. Dessa forma, definimos qual vai ser a tela, se vai ter alguma configuração de navegação, e assim por diante.

Como resultado, temos um aplicativo que consegue exibir uma tela inicial, navegar em uma tela de menu, de bebidas, de detalhes do produto, etc.

Em outras palavras: o nosso aplicativo está configurado de forma correta, mas a partir desse momento que temos um grafo de navegação e começa a expandir um pouco mais, é um ótimo momento para identificarmos novas técnicas para melhorar a forma como trabalhamos com o Navigation.

Type Safety

Iniciaremos um novo tema, chamado "Type Safety" (em português, "Digitação segura"). Vamos entender esse conceito e o motivo que o faz ser importante de implementarmos no nosso código, e quais os benefícios.

Documentação Navigation

Na documentação, temos um tópico específico falando sobre o Type Safety em "Segurança de tipo na navegação do Compose". Logo no início do parágrafo temos a seguinte afirmação: "o código nesta página (que usamos para configurar o nosso app) não tem segurança na digitação (o que chamamos de type safety)".

Como identificamos isso? Seguindo a leitura do parágrafo da documentação, temos a seguinte informação: "você pode chamar a função "navigate()" com rotas inexistentes ou argumentos incorretos".

Isso é um problema, porque lembrando que quando fazíamos uma navegação para uma rota inexistente ou incorreta, o nosso aplicativo quebrava. Esse é o comportamento padrão dentro do Navigation.

Esse é um dos motivos que nos levaram a usar as constantes, para evitarmos esse problema. Se teclarmos "Ctrl + B" somos redirecionados para o arquivo "AppDestination".

AppDestination

//código omitido

sealed class AppDestination(val route: String) {
    object Highlight : AppDestination("highlight")
    object Menu : AppDestination("menu")
    object Drinks : AppDestination("drinks")
    object ProductDetails : AppDestination("productDetails")
    object Checkout : AppDestination("checkout")
}

//código omitido

Essas constantes determinam cada destino que configuramos. Aprendemos que isso ajuda a evitar o problema mencionado. Porém, da forma que escrevemos não garante totalmente, ou seja, quando fizermos a nossa configuração mais complexa (expandindo o aplicativo) não teremos a garantia que vamos manter os resultados esperados.

Desse modo, podemos correr esses riscos. Portanto, o Type Safety auxilia na melhoria e simplificação do código escrito. Vamos entender as técnicas para fornecer essa segurança a mais quando configuramos o Navigation.

Na documentação, vamos clicar em "documentação de segurança de tipo na navegação". Seremos redirecionados para a página de Segurança de tipo na DSL do Kotlin e no Navigation Compose.

Na documentação do Type Safety, temos diversas práticas que aplicaremos ao longo do curso. Mas analisando o título, o que seria esse Kotlin DSL? A sigla "DSL" é de Domain Specific Language (em português, "Linguagem Específica de Domínio").

Quando vemos esse tipo de abreviação (DSL), o que podemos interpretar dessa informação? Todo tipo de DSL é uma linguagem da programação feita para resolver problemas específicos. Por exemplo, o SQL que é uma linguagem para realizar consultas às informações do banco de dados.

E a DSL foi criada para resolver esse problema em específico. No caso do Kotlin DSL do Type Safety, será uma forma que configuraremos o código específico para resolver esse problema da configuração de navegação. Por isso que vem com a sigla DSL.

Entendendo tudo isso, vamos compreender o que vai mudar ao que aplicamos atualmente. Como aplicamos no momento não é tão problemático assim, dado que temos as constantes e está tudo funcionando conforme o esperado.

Mas a partir de agora entenderemos os destaques para termos o ponto de partida para aplicarmos o Type Safety. No primeiro parágrafo da documentação, temos: "é necessário mapear cada tela do app ou gráfico de navegação para um arquivo de navegação por módulo".

Ou seja, em cada tela do aplicativo que é feita a configuração de destino, precisamos separar em arquivos distintos.

MainActivity

// código omitido

                            composable(AppDestination.Highlight.route) {
                                HighlightsListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                    onNavigateToCheckout = {
                                        navController.navigate(AppDestination.Checkout.route)
                                    },
                                )
                            }

// código omitido

A ideia é a seguinte: quando temos a configuração composable, determinando que é a tela de destaques (temos o composable da tela de destaques, com as configurações para navegarmos), o ideal é que todo o trecho de código anterior seja transferido para um arquivo específico.

E dentro desse arquivo, vamos aplicar as configurações envolvendo técnicas para melhorar a segurança de digitação. Podemos analisar alguns exemplos na documentação, descendo a página, temos:

Exemplo retirado da documentação do Type Safety

// ConversationScreen.kt

@Composable
internal fun ConversationScreen(
  uiState: ConversationUiState,
  onPinConversation: () -> Unit,
  onNavigateToParticipantList: (conversationId: String) -> Unit
) { ... }

Essa é a Tela de conversa, que mostra um composable assim como já fizemos. Descendo um pouco mais, temos o seguinte exemplo na documentação:

Exemplo retirado da documentação do Type Safety

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

// Adds conversation screen to `this` NavGraphBuilder
fun NavGraphBuilder.conversationScreen(
  // Navigation events are exposed to the caller to be handled at a higher level
  onNavigateToParticipantList: (conversationId: String) -> Unit
) {
  composable("conversation/{$conversationIdArg}") {
    // The ViewModel as a screen level state holder produces the screen
    // UI state and handles business logic for the ConversationScreen
    val viewModel: ConversationViewModel = hiltViewModel()
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    ConversationScreen(
      uiState,
      ::viewModel.pinConversation,
      onNavigateToParticipantList
    )
  }
}

Aqui é onde determina como é feita essa configuração para esse composable. Observe que ao invés de usar somente o NavHost dentro da MainActivity, teremos um arquivo para a tela criada (no caso do exemplo, o arquivo seria o ConversationNavigation).

E nesta tela teremos uma função de extensão (fun NavGraphBuilder.conversationScreen()) que determina que ao invés do composable, agora é uma tela. Dentro dessa tela, temos as informações encapsuladas (composable("conversation/{$conversationIdArg}"){…}).

Note que o nosso objetivo será analisar o que temos atualmente no código, e aplicar uma refatoração que fará mais sentido no momento que usarmos o Navigation no Compose.

Na sequência, iniciaremos a aplicação do Type Safety!

Melhorando a navegação com o Type Safety - Separação do grafo de navegação

Agora que conhecemos o Type Safety, o próximo passo será iniciar a refatoração para aplicar as boas práticas e as técnicas que vimos na documentação. Vamos começar?

No projeto, vamos em "app > src > main > java > br.com.alura.panucci > MainActivity.kt". O objetivo inicial é justamente separar os destinos, para que eles tenham arquivos exclusivos, assim como vimos na documentação do Type Safety.

Trecho do código selecionado pelo instrutor

MainActivity.kt

// código omitido

                            composable(AppDestination.Highlight.route) {
                                HighlightsListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                    onNavigateToCheckout = {
                                        navController.navigate(AppDestination.Checkout.route)
                                    },
                                )
                            }

// código omitido

Porém, antes de iniciarmos essa migração é importante avaliarmos um ponto que já deixamos de configuração sobre o nosso NavHost. Observe que esse NavHost possui toda configuração Navigation, sendo um pouco extensa e complexa.

O detalhe que precisamos entender é que esse tipo de configuração faz sentido. Criamos um arquivo exclusivo, ou um próprio composable que vai determinar que é o nosso próprio NavHost (para separarmos esse código e deixá-lo mais organizado).

Primeiro, antes de realizarmos a migração de cada arquivo específico para cada destino, migraremos o nosso NavHost.

Agora criaremos um composable para o NavHost dentro do pacote navigation (em "app > src > main > java > br.com.alura.panucci > navigation"). Para isso, clicamos com o botão direito no pacote "navigation" e escolhemos "Kotlin File".

No pop-up seguinte, deixamos "File" marcado e digitamos o nome "PanucciNavHost". Usamos o nome do aplicativo como prefixo. Ao teclarmos "Enter", seremos redirecionados para o arquivo que acabamos de gerar.

PanucciNavHost.kt

package br.com.alura.panucci.navigation

Podemos incluir a anotação "@Composable" que vai ter uma função PanucciNavHost.

PanucciNavHost.kt

package br.com.alura.panucci.navigation

import androidx.compose.runtime.Composable

@Composable
fun PanucciNavHost() {

}

Dentro do "PanucciNavHost()", vamos pegar o trecho de código do NavHost no arquivo MainActivity. Na linha 83, podemos clicar em "NavHost" para selecionar todo código, ou podemos usar uma técnica do Android Studio chamada seleção estendida.

Podemos habilitar isso, no Windows, usando o atalho "Ctrl + W". Observe que ao teclarmos uma vez, ele seleciona a palavra "NavHost", se teclarmos novamente "Ctrl + W" é selecionada a palavra e tudo o que está dentro seus parênteses (seus argumentos).

Clicando mais uma vez as teclas "Ctrl + W", é selecionado o corpo todo, referente ao escopo. Após selecionar, teclamos "Ctrl + X" para recortarmos o que está selecionado e voltar ao arquivo "PannucciNavHost" para colar dentro da função.

Trecho do código para copiarmos no arquivo "MainActivity"

                        NavHost(
                            navController = navController,
                            startDestination = AppDestination.Highlight.route
                        ) {
                            composable(AppDestination.Highlight.route) {
                                HighlightsListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                    onNavigateToCheckout = {
                                        navController.navigate(AppDestination.Checkout.route)
                                    },
                                )
                            }
                            composable(AppDestination.Menu.route) {
                                MenuListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                )
                            }
                            composable(AppDestination.Drinks.route) {
                                DrinksListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                )
                            }
                            composable(
                                "${AppDestination.ProductDetails.route}/{productId}"
                            ) { backStackEntry ->
                                val id = backStackEntry.arguments?.getString("productId")
                                sampleProducts.find {
                                    it.id == id
                                }?.let { product ->
                                    ProductDetailsScreen(
                                        product = product,
                                        onNavigateToCheckout = {
                                            navController.navigate(AppDestination.Checkout.route)
                                        },
                                    )
                                } ?: LaunchedEffect(Unit) {
                                    navController.navigateUp()
                                }
                            }
                            composable(AppDestination.Checkout.route) {
                                CheckoutScreen(
                                    products = sampleProducts,
                                    onPopBackStack = {
                                        navController.navigateUp()
                                    },
                                )
                            }
                        }

PanucciNavHost.kt

package br.com.alura.panucci.navigation

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import br.com.alura.panucci.sampledata.sampleProducts
import br.com.alura.panucci.ui.screens.*

@Composable
fun PanucciNavHost() {

                        NavHost(
                            navController = navController,
                            startDestination = AppDestination.Highlight.route
                        ) {
                            composable(AppDestination.Highlight.route) {
                                HighlightsListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                    onNavigateToCheckout = {
                                        navController.navigate(AppDestination.Checkout.route)
                                    },
                                )
                            }
                            composable(AppDestination.Menu.route) {
                                MenuListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                )
                            }
                            composable(AppDestination.Drinks.route) {
                                DrinksListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                )
                            }
                            composable(
                                "${AppDestination.ProductDetails.route}/{productId}"
                            ) { backStackEntry ->
                                val id = backStackEntry.arguments?.getString("productId")
                                sampleProducts.find {
                                    it.id == id
                                }?.let { product ->
                                    ProductDetailsScreen(
                                        product = product,
                                        onNavigateToCheckout = {
                                            navController.navigate(AppDestination.Checkout.route)
                                        },
                                    )
                                } ?: LaunchedEffect(Unit) {
                                    navController.navigateUp()
                                }
                            }
                            composable(AppDestination.Checkout.route) {
                                CheckoutScreen(
                                    products = sampleProducts,
                                    onPopBackStack = {
                                        navController.navigateUp()
                                    },
                                )
                            }
                        }

}

Observe que ele está com problemas para identificar o NavController (que está escrito na cor vermelha). Isso acontece porque ainda não temos o NavController.

Por isso, podemos clicar em "NavController", teclar "Alt + Enter" e clicar na opção "Create parameter 'navController'". Ao escolhermos essa possibilidade, será aberta uma janela indicando o que será criado.

No caso, ele vai criar um parâmetro do tipo NavHostController, basta clicarmos no botão "Refactor" no canto inferior direito.

PanucciNavHost.kt

package br.com.alura.panucci.navigation

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import br.com.alura.panucci.sampledata.sampleProducts
import br.com.alura.panucci.ui.screens.*

@Composable
fun PanucciNavHost(navController: NavHostController) {
                        NavHost(
                            navController = navController,
                            startDestination = AppDestination.Highlight.route
                        )
//código omitido

Pronto! Conseguimos criar o composable que contempla todo código de configuração do Navigation.

Voltando ao arquivo "MainActivity", dentro do escopo do "PanucciApp" chamaremos o "PanucciNavHost".

Trecho do código do "PanucciApp" no arquivo "MainActivity"

//código omitido

                   PanucciApp(
                        bottomAppBarItemSelected = selectedItem,
                        onBottomAppBarItemSelectedChange = {
                            val route = it.destination.route
                            navController.navigate(route) {
                                launchSingleTop = true
                                popUpTo(route)
                            }
                        },
                        onFabClick = {
                            navController.navigate(AppDestination.Checkout.route)
                        },
                        isShowTopBar = containsInBottomAppBarItems,
                        isShowBottomBar = containsInBottomAppBarItems,
                        isShowFab = isShowFab
                    ) {

                    }

//código omitido

Vamos inserir após o abrir chaves, escrevemos "PanucciNavHost(navController = navController)". Assim, o nosso trecho de código ficará:

MainActivity

//código omitido

                    PanucciApp(
                        bottomAppBarItemSelected = selectedItem,
                        onBottomAppBarItemSelectedChange = {
                            val route = it.destination.route
                            navController.navigate(route) {
                                launchSingleTop = true
                                popUpTo(route)
                            }
                        },
                        onFabClick = {
                            navController.navigate(AppDestination.Checkout.route)
                        },
                        isShowTopBar = containsInBottomAppBarItems,
                        isShowBottomBar = containsInBottomAppBarItems,
                        isShowFab = isShowFab
                    ) {
                        PanucciNavHost(navController = navController)
                    }

//código omitido

Com essa configuração feita, podemos iniciar a migração de cada destino para arquivos específicos (arquivo "PanucciNavHost").

Voltando a documentação , verificando o padrão em "Dividir o gráfico de navegação", podemos usar o exemplo "Conversation" da própria documentação.

Trecho do código de exemplo da documentação:

// ConversationScreen.kt

@Composable
internal fun ConversationScreen(
  uiState: ConversationUiState,
  onPinConversation: () -> Unit,
  onNavigateToParticipantList: (conversationId: String) -> Unit
) { ... }

Observe que o "ConversationScreen" é um composable, assim como já temos nas nossas telas. Descendo a documentação para o próximo exemplo, note que ele criou um arquivo ConversationNavigation.

Trecho do código de exemplo da documentação:

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

// Adds conversation screen to `this` NavGraphBuilder
fun NavGraphBuilder.conversationScreen(
  // Navigation events are exposed to the caller to be handled at a higher level
  onNavigateToParticipantList: (conversationId: String) -> Unit
) {
  composable("conversation/{$conversationIdArg}") {
    // The ViewModel as a screen level state holder produces the screen
    // UI state and handles business logic for the ConversationScreen
    val viewModel: ConversationViewModel = hiltViewModel()
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    ConversationScreen(
      uiState,
      ::viewModel.pinConversation,
      onNavigateToParticipantList
    )
  }
}

Então se temos o nosso highlightsList, teremos o "highlightsList.navigation". Se tivermos o menu, temos o "menu.navigation" e assim por diante. É justamente isso que precisamos fazer!

Note que as funções que vamos separar são funções estendidas do NavGraphBuilder. O que é esse NavGraphBuilder? É a referência a que temos acesso quando chamamos o NavHost. Neste, temos como último parâmetro (no caso o builder) que vai ter um tipo de função que será a extensão de NavGraphBuilder.

Logo, precisamos dele para chamar o composable responsáveis por configurar o destino. Por isso precisa ser uma função de extensão. Antes de criar esse arquivo, uma das técnicas que podemos usar é selecionar cada grafo no arquivo "PanucciNavHost" e usar o atalho de extrair métodos (Ctrl + Alt + M).

Grafo selecionado pelo instrutor no arquivo "PanucciNavHost"

//código omitido

                            composable(AppDestination.Highlight.route) {
                                HighlightsListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                    onNavigateToCheckout = {
                                        navController.navigate(AppDestination.Checkout.route)
                                    },
                                )
                            }

//código omitido

Após teclarmos, ele já aciona de forma automática uma configuração de função de extensão (expressão lambda) no nosso NavGraphBuilder. No campo "visibility" deixaremos como "public" (em português, "público") e não privado, porque outras pessoas precisam acessar. E no campo "Nome" digitamos "highlightsListScreen". Logo após, clicamos no botão "Ok".

PanucciNavHost

//código omitido

                        NavHost(
                            navController = navController,
                            startDestination = AppDestination.Highlight.route
                        )
                            highlightsListScreen(navController)
                            composable(AppDestination.Menu.route) {
                                MenuListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                )
                            }

                            }

//código omitido

Agora, sim, estamos com o padrão que vimos do nosso Type Safety. Porém, há alguns detalhes que precisamos nos atentar ao extrair dessa forma.

Descendo o código, observe que ele coloca como se fosse o composable. Por padrão, ele aplica essa estrutura e não temos muito o que alterar quando estamos no escopo do composable.

PanucciNavHost

// código omitido

@Composable
fun NavGraphBuilder.highlightsListScreen(navController: NavHostController) {
    composable(AppDestination.Highlight.route) {
        HighlightsListScreen(
                products = sampleProducts, 
                onNavigationToDetails = { product -> 
                    navController.navigate(
                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                        onNavigateToCheckout = {
                                            navController.navigate(AppDestination.Checkout.route)
                                        },
                                    )
                                                                }
                                                        }

Mas dentro dessa expressão lambda, já não estamos mais num escopo de composable. Por isso, precisamos remover. Essa é a desvantagem de aplicar essa técnica, mas ele já adiantou bastante coisa para nós.

O que falta é criarmos o arquivo desse nosso destino, faremos isso para os outros também. Mas agora faremos para essa função de extensão que fizemos inicial para a tela de destaques.

Do lado esquerdo, clicamos com o botão direito no pacote navigation e escolhemos a opção "Kotlin Class/File". Nomearemos de "HighlightsListNavigation" e teclamos "Enter". Seremos redirecionados para o arquivo:

HighlightsListNavigation

package br.com.alura.panucci.navigation

Com esse arquivo criado, voltamos ao "PanucciNavHost" e recortamos do código o trecho com a função NavGraphBuilder (nossa função de extensão) para colarmos em "HighlightsListNavigation".

HighlightsListNavigation

package br.com.alura.panucci.navigation

import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import br.com.alura.panucci.sampledata.sampleProducts
import br.com.alura.panucci.ui.screens.HighlightsListScreen

fun NavGraphBuilder.highlightsListScreen(navController: NavHostController) {
    composable(AppDestination.Highligh.route) {
        HighlightsListScreen(
            products = sampleProducts,
            onNavigateToDetails = { product ->
                navController.navigate(
                                    "${AppDestination.ProductDetails.route}/${product.id}"
                                )
            },
            onNavigateToCheckout = {
                navController.navigate(AppDestination.Checkout.route)
            },
        )
    }
}

O primeiro passo está feito, agora faremos isso para os outros composables. Observe no arquivo "PanucciNavHost" que com essa configuração, fica claro que estamos trabalhando com a nossa HighlightsListScreen.

Para fazermos isso com os de mais, selecionamos o menu:

PanucciNavHost

// código omitido

    composable(AppDestination.Menu.route) {
        MenuListScreen(
                products = sampleProducts, 
                onNavigationToDetails = { product -> 
                    navController.navigate(
                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                                                    )
                                                                }

Com esse trecho selecionado, teclamos "Ctrl + Alt + M". No campo "Nome" digitamos "menuScreen". O código ficará:

PanucciNavHost

//código omitido

    NavHost(
        navController = navController,
        startDestination = AppDestination.Highlight.route
    ) {
        highlightsListScreen(navController)
        menuScreen(navController)

composable(AppDestination.Drinks.route) {
                                DrinksListScreen(
                                    products = sampleProducts,
                                    onNavigateToDetails = { product ->
                                        navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                        )
                                    },
                                )
                            }

//código omitido

Agora selecionamos todo o composable de "Drinks" e teclamos "Ctrl + Alt + M". Nomearemos de "drinksScreen" e clicamos no botão "Ok".

PanucciNavHost

//código omitido

    NavHost(
        navController = navController,
        startDestination = AppDestination.Highlight.route
    ) {
        highlightsListScreen(navController)
        menuScreen(navController)
        drinksScreen(navController)

//código omitido

Logo após, faremos o mesmo processo para "ProductDetails".

PanucciNavHost

//código omitido

                            composable(
                                "${AppDestination.ProductDetails.route}/{productId}"
                            ) { backStackEntry ->
                                val id = backStackEntry.arguments?.getString("productId")
                                sampleProducts.find {
                                    it.id == id
                                }?.let { product ->
                                    ProductDetailsScreen(
                                        product = product,
                                        onNavigateToCheckout = {
                                            navController.navigate(AppDestination.Checkout.route)
                                        },
                                    )
                                } ?: LaunchedEffect(Unit) {
                                    navController.navigateUp()
                                }
                            }

//código omitido

Selecionamos, teclamos "Ctrl + Alt + M" e nomeamos de "productDetailsScreen". Assim, o trecho do código ficará:

PanucciNavHost

//código omitido

    NavHost(
        navController = navController,
        startDestination = AppDestination.Highlight.route
    ) {
        highlightsListScreen(navController)
        menuScreen(navController)
        drinksScreen(navController)
                productDetailsScreen(navController)

//código omitido

Mais uma vez, mas agora em "Checkout". Selecionamos o seguinte trecho do código, teclamos "Ctrl + Alt + M" e nomeamos de "checkoutScreen".

PanucciNavHost

//código omitido

composable(AppDestination.Checkout.route) {
                                CheckoutScreen(
                                    products = sampleProducts,
                                    onPopBackStack = {
                                        navController.navigateUp()
                                    },
                                )
                            }

//código omitido

Com isso, temos todos os nossos métodos! Agora precisamos colocar os arquivos para testar.

PanucciNavHost

package br.com.alura.panucci.navigation

import androidx.navigation.compose.composable
import br.com.alura.panucci.sampledata.sampleProducts
import br.com.alura.panucci.ui.screens.*

@Composable
fun PanucciNavHost(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = AppDestination.Highlight.route
    ) {
        highlightsListScreen(navController)
        menuScreen(navController)
        drinksScreen(navController)
        productDetailsScreen(navController)
        checkoutScreen(navController)
    }

}

@Composable
private fun NavGraphBuilder.checkoutScreen(navController: NavHostController) {
    composable(AppDestination.Checkout.route) {
        CheckoutScreen(
                products = sampleProducts, 
                onPopBackStack = {
                        navController.navigateUp()
        },
       )
            }
        }

//código omitido

Nos composables do arquivo, podemos remover as anotações "@Composable" e o "private". Assim, ficamos com:

PanucciNavHost

// código omitido

fun NavGraphBuilder.checkoutScreen(navController: NavHostController) {
    composable(AppDestination.Checkout.route) {
        CheckoutScreen(
                products = sampleProducts, 
                onPopBackStack = {
                        navController.navigateUp()
        },
       )
            }
        }

// código omitido

Iniciaremos por esse checkoutScreen. Criamos um arquivo para ele selecionando o pacote "navigation" do lado esquerdo e nomeando o arquivo Kotlin de "CheckoutNavigation". Após teclarmos "Enter", somos redirecionados para o arquivo:

CheckoutNavigation

package br.com.alura.panucci.navigation

Depois de criar o arquivo "CheckoutNavigation", copiamos a função de extensão do arquivo "PanucciNavHost" e colamos nele.

CheckoutNavigation

package br.com.alura.panucci.navigation

import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import br.com.alura.panucci.sampledata.sampleProducts
import br.com.alura.panucci.ui.screens.CheckoutScreen

fun NavGraphBuilder.checkoutScreen(navController: NavHostController) {
    composable(AppDestination.Checkout.route) {
        CheckoutScreen(
                products = sampleProducts, 
                onPopBackStack = {
                        navController.navigateUp()
        },
       )
            }
        }

Faremos o mesmo para tela de Menu. Vamos criar um arquivo dentro do pacote navigation nomeado "MenuNavigation".

MenuNavigation

package br.com.alura.panucci.navigation

Agora vamos ao arquivo "PanucciNavHost" e copiamos a função de extensão de Menu.

PanucciNavHost

//código omitido

fun NavGraphBuilder.menuScreen(navController: NavHostController) {
    composable(AppDestination.Menu.route) {
        MenuListScreen(
            products = sampleProducts,
            onNavigateToDetails = { product ->
                navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                )
            },
        )
    }
}

//código omitido

MenuNavigation

package br.com.alura.panucci.navigation

import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import br.com.alura.panucci.sampledata.sampleProducts
import br.com.alura.panucci.ui.screens.MenuListScreen

fun NavGraphBuilder.menuScreen(navController: NavHostController) {
    composable(AppDestination.Menu.route) {
        MenuListScreen(
            products = sampleProducts,
            onNavigateToDetails = { product ->
                navController.navigate(
                                            "${AppDestination.ProductDetails.route}/${product.id}"
                                )
            },
        )
    }
}

Perfeito! Agora faremos para "Drinks", criando um arquivo chamado "DrinksNavigation" dentro do pacote navigation. Pegamos o "drinksScreen" no arquivo "PanucciNavHost" e colamos dentro do arquivo que acabamos de criar.

DrinksNavigation

package br.com.alura.panucci.navigation

import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import br.com.alura.panucci.sampledata.sampleProducts
import br.com.alura.panucci.ui.screens.DrinksListScreen

fun NavGraphBuilder.drinksScreen(navController: NavHostController) {
    composable(AppDestination.Drinks.route) {
        DrinksListScreen(
            products = sampleProducts,
            onNavigateToDetails = { product ->
                navController.navigate(  "${AppDestination.ProductDetails.route}/${product.id}"
                                )
            },
        )
    }
}

Até o momento, temos os seguintes arquivos dentro do pacote navigation:

Todo esse processo que estamos fazendo, se fosse com um grafo enorme, ficaria muito mais complexo de fazer. Portanto, é interessante entendermos esses exemplos menores e começarmos a compreender o valor dessa configuração.

Ficou faltando apenas os detalhes dos produtos!

Do lado esquerdo, criaremos o arquivo "ProductDetailsNavigation" e faremos o mesmo processo que todos os anteriores. Ficaremos com o seguinte arquivo:

ProductDetailsNavigation

package br.com.alura.panucci.navigation`

import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import br.com.alura.panucci.sampledata.sampleProducts
import br.com.alura.panucci.ui.screens.ProductDetailsScreen

fun NavGraphBuilder.productDetailsScreen(navController: NavHostController) {
                            composable(
                                "${AppDestination.ProductDetails.route}/{productId}"
                            ) { backStackEntry ->
                                val id = backStackEntry.arguments?.getString("productId")
                                sampleProducts.find {
                                    it.id == id
                                }?.let { product ->
                                    ProductDetailsScreen(
                                        product = product,
                                        onNavigateToCheckout = {
                                            navController.navigate(AppDestination.Checkout.route)
                                        },
                                    )
                                } ?: LaunchedEffect(Unit) {
                                    navController.navigateUp()
                                }
                            }
}

Arquivo "PanucciNavHost" no GitHub

Pasta "Navigation" no GitHub

Temos todos os nossos arquivos! Agora vamos verificar como ficou o arquivo "PanucciNavHost".

PanucciNavHost

package br.com.alura.panucci.navigation

import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost

@Composable
fun PanucciNavHost(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = highlightsListRoute
    ) {
        highlightsListScreen(navController)
        menuScreen(navController)
        drinksScreen(navController)
        productDetailsScreen(navController)
        checkoutScreen(navController)
    }

}

Observe que a leitura ficou mais simplificada. Temos uma lista de menu, destaques, bebidas, detalhes do produto e a tela do pedido. Ou seja, temos um grande impacto ao aplicarmos essa primeira configuração.

Podemos usar o atalho "Ctrl + Alt + O", para remover os imports desnecessários. Agora é necessário testarmos para analisar se o nosso app ainda funciona.

O app rodou, acessaremos todas as telas, de Menu, Destaques, Bebidas, Detalhes do produto e Pedidos. Isso significa que ele está funcionando, mas iniciamos a primeira configuração para aplicar as boas práticas e técnicas do Type Safety.

Sobre o curso Jetpack Compose: Navigation com Type Safety

O curso Jetpack Compose: Navigation com Type Safety possui 142 minutos de vídeos, em um total de 48 atividades. Gostou? Conheça nossos outros cursos de Android em Mobile, ou leia nossos artigos de Mobile.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Android acessando integralmente esse e outros cursos, comece hoje!

Plus

De
R$ 1.800
12X
R$109
à vista R$1.308
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

Matricule-se

Pro

De
R$ 2.400
12X
R$149
à vista R$1.788
  • Acesso a TODOS os cursos da Alura

    Mais de 1500 cursos completamente atualizados, com novos lançamentos todas as semanas, emProgramação, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.

  • Alura Challenges

    Desafios temáticos para você turbinar seu portfólio. Você aprende na prática, com exercícios e projetos que simulam o dia a dia profissional.

  • Alura Cases

    Webséries exclusivas com discussões avançadas sobre arquitetura de sistemas com profissionais de grandes corporações e startups.

  • Certificado

    Emitimos certificados para atestar que você finalizou nossos cursos e formações.

  • Luri powered by ChatGPT

    Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com Luri até 100 mensagens por semana.

  • Alura Língua (incluindo curso Inglês para Devs)

    Estude a língua inglesa com um curso 100% focado em tecnologia e expanda seus horizontes profissionais.

Matricule-se
Conheça os Planos para Empresas

Acesso completo
durante 1 ano

Estude 24h/dia
onde e quando quiser

Novos cursos
todas as semanas