Como navegar com o Compose

O componente de navegação oferece suporte a 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 suporte ao Compose, use a dependência abaixo no arquivo build.gradle do módulo do app:

Groovy

dependencies {
    def nav_version = "2.5.3"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.5.3"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

Para começar

NavController é a API central do componente de navegação. É uma função com estado que acompanha a backstack de elementos combináveis, que, por sua vez, criam as telas do app e o estado de cada uma delas.

É possível criar uma NavController usando o método rememberNavController() no elemento de composição:

val navController = rememberNavController()

Crie a NavController em um local na hierarquia de elementos combináveis em que todos os elementos que precisam referenciá-la tenham acesso a ela. Isso está de acordo com os princípios da elevação de estado e permite que você use a NavController e o estado apresentado por ela via currentBackStackEntryAsState() para que essa função seja usada como a fonte da verdade para atualizar elementos combináveis fora das telas. Consulte Integração com a barra de navegação inferior para ver um exemplo dessa funcionalidade.

Como criar um NavHost

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

Para criar NavHost é necessário informar a NavController criada anteriormente por rememberNavController() e a rota do destino inicial do gráfico. A criação de NavHost usa a sintaxe lambda da DSL Kotlin de navegação para construir seu gráfico de navegação. É possível adicionar à estrutura de navegação usando o método composable(). Ele exige uma rota e o elemento de composição vinculado 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 de composição. 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:

navController.navigate("friendslist")

Por padrão, navigate adiciona seu novo destino à backstack. 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 "friendslist" destination
navController.navigate("friendslist") {
    popUpTo("home")
}

// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friendslist" destination
navController.navigate("friendslist") {
    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 mais casos de uso.

A função navigate da NavController modifica o estado interno da NavController. Para obedecer ao princípio de fonte única da verdade o máximo possível, somente a função combinável ou o detentor de estado que eleva a instância NavController e as funções combináveis que recebem o NavController como parâmetro precisam fazer chamadas de navegação. Eventos de navegação acionados por outras funções de composição mais abaixo na hierarquia da IU precisam expor esses eventos ao autor da chamada de forma adequada usando funções.

O exemplo abaixo mostra a função de composição MyAppNavHost como a única fonte de verdade da instância NavController. ProfileScreen expõe um evento como uma função que é chamada quando o usuário toca em um botão. A função MyAppNavHost, que cuida da navegação para as diferentes telas no app, faz a chamada para o destino correto ao chamar ProfileScreen.

@Composable
fun MyAppNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController = rememberNavController(),
    startDestination: String = "profile"
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        composable("profile") {
            ProfileScreen(
                onNavigateToFriends = { navController.navigate("friendsList") },
                /*...*/
            )
        }
        composable("friendslist") { FriendsListScreen(/*...*/) }
    }
}

@Composable
fun ProfileScreen(
    onNavigateToFriends: () -> Unit,
    /*...*/
) {
    /*...*/
    Button(onClick = onNavigateToFriends) {
        Text(text = "See friends list")
    }
}

Chame navigate() apenas como parte de um callback e não como parte do elemento combinável para evitar chamar navigate() em cada recomposição.

A exposição de eventos de funções de composição a autores de chamada que sabem como processar uma determinada lógica no app é uma prática recomendada do Compose para elevação de estado.

Embora a exposição de eventos como parâmetros lambda individuais possa sobrecarregar a assinatura da função, ela maximiza a visibilidade das responsabilidades das funções de composição. Você pode conferir facilmente o que elas fazem.

Outras alternativas que podem reduzir o número de parâmetros na declaração da função são até mais confortáveis inicialmente para escrever, mas têm algumas desvantagens a longo prazo difíceis de identificar. Por exemplo, criar uma classe de wrapper, como ProfileScreenEvents, que centraliza todos os eventos em um só lugar. Isso reduz a visibilidade do que o elemento de composição faz ao analisar a definição da função, adiciona outra classe e métodos à contagem de projetos, e é necessário criar e lembrar instâncias dessa classe toda vez você chama essa função de composição mesmo assim. Além disso, para reutilizar essa classe de wrapper o máximo possível, esse padrão incentiva a transmissão de uma instância dessa classe para uma hierarquia de IU em vez da prática recomendada de transmitir para elementos combináveis apenas o que eles precisam.

O Navigation Compose também permite transmitir argumentos entre destinos de composição. Para fazer isso, adicione 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. O parâmetro arguments de composable() aceita uma lista de NamedNavArguments. É possível criar rapidamente uma NamedNavArgument usando o método navArgument e, em seguida, especificar o type exato:

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

Extraia os argumentos 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 adicioná-lo à rota ao fazer a chamada navigate:

navController.navigate("profile/user1234")

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

Como recuperar dados complexos durante a navegação

É altamente recomendável não transmitir objetos de dados complexos ao navegar. Em vez disso, transmita as informações mínimas necessárias, como um identificador exclusivo ou outra forma de ID, como argumentos ao executar ações de navegação:

// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")

Objetos complexos precisam ser armazenados na forma de dados em uma única fonte de verdade, como a camada de dados. Depois de acessar o destino após a navegação, é possível carregar as informações necessárias da única fonte de verdade usando o ID transmitido. Para recuperar os argumentos responsáveis por acessar a camada de dados no ViewModel, use o ViewModel’s SavedStateHandle:

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

// …

}

Essa abordagem ajuda a evitar a perda de dados durante mudanças de configuração e qualquer inconsistência quando o objeto em questão está sendo atualizado ou modificado.

Para uma explicação mais detalhada sobre por que evitar a transmissão de dados complexos como argumentos e uma lista de tipos de argumentos aceitos, 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 = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Mesmo que não haja nenhum argumento transmitido para o destino, o defaultValue, "user1234", vai ser usado.

A estrutura do processamento dos argumentos pelas rotas permite que os elementos de composição permaneçam completamente independentes da navegação e 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(). O parâmetro deepLinks aceita uma lista de NavDeepLinks que podem ser criados rapidamente usando o método navDeepLink:

val uri = "https://www.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 ou um tipo MIME específico a um elemento de composição. 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 código abaixo ao elemento <activity> do manifesto:

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

A navegação automaticamente estabelece um link direto para esse elemento combinável quando o link é acionado por outro app.

Esses mesmos links diretos também podem ser usados para criar um PendingIntent com o link direto adequado de um elemento combinável:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.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 vão poder 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, ele pode ser usado com outros pré-criados, como 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 composição, você pode conectar o Navigation a outros componentes, como o componente de navegação inferior. Isso permite que você selecione os ícones na barra de navegação interior.

Para usar os componentes BottomNavigation e BottomNavigationItem, adicione a dependência androidx.compose.material ao seu app Android.

Groovy

dependencies {
    implementation "androidx.compose.material:material:1.3.1"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.4.3"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.3.1")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.4.3"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Para vincular as rotas no gráfico de navegação aos itens de uma barra de navegação inferior, é 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 de composição, 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 a hierarquia NavDestination.

A rota do item também é usada para conectar a lambda onClick a uma chamada para navigate. Assim, ao tocar no item você vai 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.

Segurança de tipo na navegação do Compose

O código nesta página não tem segurança de tipo. Você pode chamar a função navigate() com rotas inexistentes ou argumentos incorretos. No entanto, é possível estruturar o código de navegação para que ele tenha segurança de tipo durante a execução. Dessa forma, você evita falhas e garante que:

  • Os argumentos fornecidos ao navegar para um gráfico de destino ou navegação sejam do tipo certo e que todos os argumentos necessários estejam presentes.
  • Os argumentos extraídos de SavedStateHandle sejam do tipo correto.

Para mais informações, consulte a documentação de segurança de tipo na navegação.

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 forem combináveis.

Portanto, a recomendação para apps mistos do Compose e de visualizações é usar o componente de navegação baseado em fragmentos. Os fragmentos contêm telas com base em visualizações, telas do Compose e telas que usam o Compose e as visualizações. Quando o conteúdo de cada fragmento está no Compose, a próxima etapa é 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 combinável na hierarquia:

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

No fragmento, você procura a NavController e navega até o destino para criar uma ponte entre o Compose e o componente de navegação baseado em fragmento:

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.

Testes

Recomendamos que você desassocie o código do Navigation dos destinos de composição para permitir o teste de cada elemento de composição isoladamente, separado do NavHost de composição.

Isso significa que não é recomendado transmitir navController diretamente para qualquer elemento de composição. Em vez disso, transmita callbacks de navegação como parâmetros. Isso permite que todos os elementos de composição sejam testáveis individualmente, já que não exigem uma instância de navController em testes.

O nível de indireção fornecido pela lambda composable é o que permite separar o código de navegação do elemento combinável. Isso funciona em duas direções:

  • Transmitir para a função de composição apenas argumentos analisados
  • Transmitir lambdas que vão ser acionadas pelo elemento de composição para navegar, em vez da NavController em si.

Por exemplo, um Profile de composição 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 seguinte assinatura:

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

Dessa forma, o elemento de composição Profile funciona de maneira independente da navegação, permitindo que ele seja testado em separado. A lambda composable encapsula a lógica mínima necessária para preencher a lacuna entre as APIs de navegação e o elemento de composição.

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

É recomendável programar testes que atendam aos requisitos de navegação do seu app testando o NavHost, as ações de navegação transmitidas para os elementos e as telas individuais de composição.

Como testar o NavHost

Para começar a testar seu NavHost, adicione a seguinte dependência de testes de navegação:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

É possível configurar o assunto do teste NavHost e transmitir uma instância da instância navController para ele. O artefato de teste do Navigation fornece um TestNavHostController para conseguir isso. Um teste de IU que verifica o destino inicial do app e NavHost teria esta aparência:

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: TestNavHostController

    @Before
    fun setupAppNavHost() {
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            AppNavHost(navController = navController)
        }
    }

    // Unit test
    @Test
    fun appNavHost_verifyStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Start Screen")
            .assertIsDisplayed()
    }
}

Como testar ações de navegação

Você pode testar a implementação da navegação de várias maneiras. Para isso, clique nos elementos da IU e confira o destino mostrado ou compare a rota esperada com a rota exibida.

Como o objetivo é testar a implementação concreta do app, recomendamos clicar na IU. Para aprender a testar isso de forma isolada com funções de composição individuais, confira o codelab Como testar no Jetpack Compose.

Também é possível usar o navController para conferir suas declarações ao comparar a rota de string atual com a esperada, usando a currentBackStackEntry de navController:

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "profiles")
}

Para ver mais orientações sobre os conceitos básicos de testes no Compose, consulte a documentação sobre Testes do Compose e o codelab Como testar no Jetpack Compose. Para saber mais sobre testes avançados do código de navegação, consulte o guia Testar a navegação.

Saiba mais

Para saber mais sobre a navegação do Jetpack, consulte Como usar o componente de navegação ou faça o codelab de navegação do Jetpack Compose.

Para aprender a projetar a navegação do app para que ele se adapte a diferentes tamanhos, orientações e formatos de tela, consulte Navegação para IUs responsivas.

Para saber mais sobre a implementação mais avançada do Navigation Compose em um app modularizado, incluindo conceitos como gráficos aninhados e integração da barra de navegação inferior, consulte o repositório Now in Android (em inglês).

Exemplos