Como navegar com o Compose

O componente de navegação é compatível com aplicativos do Jetpack Compose. É possível navegar entre funções que podem ser compostas e, ao mesmo tempo, aproveitar a infraestrutura e os recursos do componente de navegação.

Configurar

Para oferecer compatibilidade com o Compose, use a dependência a seguir no arquivo build.gradle do módulo do app:

Groovy

dependencies {
    implementation "androidx.navigation:navigation-compose:2.4.0-alpha10"
}

Kotlin

dependencies {
    implementation("androidx.navigation:navigation-compose:2.4.0-alpha10")
}

Primeiros passos

NavController é a API central do componente Navigation. É uma função com estado que acompanha a pilha de funções que podem ser compostas, as quais, por sua vez, criam as telas do app e o estado de cada tela.

É possível criar um NavController usando o método rememberNavController() na função que pode ser composta:

val navController = rememberNavController()

Na hierarquia de funções que podem ser compostas, crie o NavController em um local onde todas as funções que precisem referenciá-lo tenham acesso a ele. Isso está de acordo com os princípios da elevação de estado e permite que você use o NavController e o estado apresentado por ele por currentBackStackEntryAsState(), para que essa função seja usada como a fonte da verdade para atualizar funções que podem ser compostas fora das telas. Consulte Integração com a barra de navegação inferior para ver um exemplo dessa funcionalidade função

Como criar um NavHost

Cada NavController precisa ser associado a um único NavHost que pode ser composto. NavHost vincula NavController a um gráfico de navegação que especifica entre quais destinos que podem ser compostos você pode navegar. À medida que você navega entre as funções que podem ser compostas, o conteúdo do NavHost é automaticamente recomposto. Cada destino composto no gráfico de navegação está associado a uma rota.

Para criar NavHost é necessário informar o NavController criado anteriormente por rememberNavController() e a rota do destino inicial do gráfico. A criação de NavHost usa a sintaxe lambda da DSL Kotlin do Navigation para construir seu gráfico de navegação. É possível adicionar à estrutura de navegação usando o método composable(). Esse método exige uma rota e a função que pode ser composta que estará vinculada ao destino:

NavHost(navController = navController, startDestination = "profile") {
    composable("profile") { Profile(/*...*/) }
    composable("friendslist") { FriendsList(/*...*/) }
    /*...*/
}

No gráfico de navegação, use o método navigate() para navegar até um destino que pode ser composto. navigate() usa um único parâmetro String que representa a rota do destino. Para navegar de um destino dentro do gráfico de navegação, chame navigate():

@Composable
fun Profile(navController: NavController) {
    /*...*/
    Button(onClick = { navController.navigate("friends") }) {
        Text(text = "Navigate next")
    }
    /*...*/
}

Chame navigate() apenas como parte de um callback e não como parte do elemento que pode ser composto, a fim de evitar chamar navigate() em cada recomposição.

Por padrão, navigate() adiciona seu novo destino à pilha de retorno. Você pode modificar o comportamento de navigate anexando outras opções de navegação à nossa chamada navigate():

// Pop everything up to the "home" destination off the back stack before
// navigating to the "friends" destination
navController.navigate(“friends”) {
    popUpTo("home")
}

// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friends" destination
navController.navigate("friends") {
    popUpTo("home") { inclusive = true }
}

// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
    launchSingleTop = true
}

Consulte o guia do popUpTo para ver mais casos de uso.

O Navigation Compose também permite transmitir argumentos entre destinos que podem ser compostos. Para fazer isso, é necessário adicionar marcadores de argumentos à rota, de forma semelhante a como você adiciona argumentos a um link direto ao usar a biblioteca de navegação de base:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

Por padrão, todos os argumentos são analisados como strings É possível especificar outro tipo usando o parâmetro arguments para definir um type:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

Extraia NavArguments de NavBackStackEntry, disponível no lambda da função composable().

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Para transmitir o argumento ao destino, é preciso adicionar o valor à rota no lugar do marcador na chamada para navigate:

navController.navigate("profile/user1234")

Para ver uma lista de tipos compatíveis, consulte Transmitir dados entre destinos.

Como adicionar argumentos opcionais

O Navigation Compose também é compatível com argumentos de navegação opcionais. Os argumentos opcionais são diferentes dos obrigatórios de duas maneiras:

  • Eles precisam ser incluídos usando a sintaxe de parâmetros de consulta ("?argName={argName}")
  • Eles precisam ter um conjunto de defaultValue ou ser definidos como nullability = true (que define o valor padrão como null de forma implícita)

Isso significa que todos os argumentos opcionais precisam ser adicionados de forma explícita à função composable() como uma lista:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Mesmo que não haja nenhum argumento transmitido para o destino, o defaultValue de "me" será usado.

A estrutura do processamento dos argumentos pelas rotas permite que as funções que podem ser compostas permaneçam completamente independentes do Navigation e sejam muito mais testáveis.

O Navigation Compose é compatível com links diretos implícitos, que também podem ser definidos como parte da função composable(). Adicione esses links como uma lista usando navDeepLink():

val uri = "https://example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

Esses links diretos permitem associar um URL, uma ação e/ou um tipo MIME específico a uma função que pode ser composta. Por padrão, esses links diretos não são expostos a apps externos. Para disponibilizar esses links diretos externamente, adicione os elementos <intent-filter> adequados ao arquivo manifest.xml do app. Para ativar o link direto acima, adicione o seguinte ao elemento <activity> do manifesto:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

O Navigation automaticamente terá um link direto para a função que pode ser composta quando o link for acionado por outro app.

Esses mesmos links diretos também podem ser usados para criar um PendingIntent com o link direto adequado de uma função que pode ser composta:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

deepLinkPendingIntent pode ser usada da mesma forma que qualquer outra PendingIntent para abrir seu app no destino do link direto.

Navegação aninhada

Os destinos podem ser agrupados em um gráfico aninhado para modularizar um fluxo específico na IU do app. Um exemplo disso é um fluxo de login independente.

O gráfico aninhado encapsula os próprios destinos. Como no gráfico raiz, um gráfico aninhado precisa ter um destino identificado como o inicial pela rota dele. Este é o destino que é acessado quando você navega até a rota associada ao gráfico aninhado.

Para adicionar um gráfico aninhado ao seu NavHost, use a função de extensão navigation:

NavHost(navController, startDestination = "home") {
    ...
    // Navigating to the graph via its route ('login') automatically
    // navigates to the graph's start destination - 'username'
    // therefore encapsulating the graph's internal routing logic
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
    ...
}

É altamente recomendável dividir seu gráfico de navegação em vários métodos à medida que o tamanho dele aumenta. Dessa forma, vários módulos também poderão contribuir com gráficos de navegação próprios.

fun NavGraphBuilder.loginGraph(navController: NavController) {
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
}

Ao transformar o método em um método de extensão em NavGraphBuilder, você pode o usar com os métodos de extensão pré-criados navigation, composable e dialog:

NavHost(navController, startDestination = "home") {
    ...
    loginGraph(navController)
    ...
}

Integração com a barra de navegação inferior

Ao definir o NavController em um nível superior na hierarquia de funções que podem ser compostas, é possível conectar o Navigation a outros componentes, como BottomNavBar. Isso permite que você selecione os ícones na barra de navegação interior.

Para vincular os itens em uma barra de navegação inferior às rotas no gráfico de navegação, é recomendável definir uma classe selada, como a Screen mencionada aqui, que contenha o ID de recurso da string e da rota para o destinos.

sealed class Screen(val route: String, @StringRes val resourceId: Int) {
    object Profile : Screen("profile", R.string.profile)
    object FriendsList : Screen("friendslist", R.string.friends_list)
}

Em seguida, coloque esses itens em uma lista que possa ser usada pelo BottomNavigationItem:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

No seu BottomNavigation que pode ser composto, extraia a NavBackStackEntry usando a função currentBackStackEntryAsState(). Essa entrada fornece acesso ao NavDestination atual. O estado selecionado de cada BottomNavigationItem pode ser determinado comparando a rota do item com a rota do destino atual e os destinos pai dele, para processar casos em que você está usando a navegação aninhada com o método auxiliar hierarchy.

A rota do item também é usada para conectar a lambda onClick a uma chamada para navigate. Assim, ao tocar no item você navegará até ele. Usando as sinalizações saveState e restoreState, o estado e a backstack desse item são salvos e restaurados corretamente à medida que você alterna entre os itens da navegação inferior.

val navController = rememberNavController()
Scaffold(
  bottomBar = {
    BottomNavigation {
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      items.forEach { screen ->
        BottomNavigationItem(
          icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = { Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
          onClick = {
            navController.navigate(screen.route) {
              // Pop up to the start destination of the graph to
              // avoid building up a large stack of destinations
              // on the back stack as users select items
              popUpTo(navController.graph.findStartDestination().id) {
                saveState = true
              }
              // Avoid multiple copies of the same destination when
              // reselecting the same item
              launchSingleTop = true
              // Restore state when reselecting a previously selected item
              restoreState = true
            }
          }
        )
      }
    }
  }
) { innerPadding ->
  NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
    composable(Screen.Profile.route) { Profile(navController) }
    composable(Screen.FriendsList.route) { FriendsList(navController) }
  }
}

Aqui você aproveita o método NavController.currentBackStackEntryAsState() para elevar o estado de navController para fora da função NavHost e compartilhá-lo com o componente BottomNavigation. Isso significa que BottomNavigation automaticamente tem o estado mais atualizado.

Interoperabilidade

Se quiser usar o componente de navegação com o Compose, você tem duas opções:

  • Definir um gráfico de navegação com o componente de navegação para fragmentos.
  • Definir um gráfico de navegação com um NavHost no Compose usando destinos do Compose. Isso só é possível se todas as telas no gráfico de navegação puderem ser compostas.

Portanto, a recomendação para apps híbridos é usar o componente de navegação com base em fragmentos e usar fragmentos para armazenar telas com base em visualização, telas do Compose e telas que usam ambos os tipos de visualizações e o Compose. Quando cada fragmento de tela no app for um wrapper em torno de uma função que pode ser composta, a próxima etapa será unir todas essas telas ao Navigation Compose e remover todos os fragmentos.

Para alterar os destinos no código do Compose, você expõe eventos que podem ser transmitidos e acionados por qualquer função que pode ser composta na hierarquia:

@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

No fragmento, você transforma a ponte entre o Compose e o componente de navegação com base em fragmento encontrando o NavController e navegando até o destino:

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

Como alternativa, você pode transmitir o NavController pela hierarquia do Compose. No entanto, a exposição de funções simples é muito mais reutilizável e testável.

Como testar

Recomendamos que você desassocie o código do Navigation dos destinos que podem ser compostos, para permitir o teste de cada composto isoladamente, separados do NavHost que pode ser composto.

O nível de indireção fornecido pelo lambda composable é o que permite separar o código do Navigation da função que pode ser composta. Isso funciona em duas direções:

  • Transmitir apenas argumentos analisados para a função que pode ser composta
  • Passar lambdas que serão acionados pela função que pode ser composta para navegar, em vez do próprio NavController.

Por exemplo, um Profile que pode ser composto que usa um userId como entrada e permite que os usuários naveguem até a página de perfil de um amigo pode ter a assinatura de:

@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 …
}

É possível ver que o Profile que pode ser composto funciona de forma independente do Navigation, permitindo que ele seja testado de forma independente. O lambda composable encapsula a lógica mínima necessária para preencher a lacuna entre as APIs Navigation e a função que pode ser composta:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

Saiba mais

Para saber mais sobre o Jetpack Navigation, consulte Primeiros passos com o componente de navegação ou faça o codelab do Jetpack Compose Navigation.