Navigazione con Scrivi

Il componente Navigazione fornisce supporto per le applicazioni Jetpack Compose. Puoi spostarti tra i componenti componibili sfruttando al contempo l'infrastruttura e le funzionalità del componente di navigazione.

Configura

Per supportare Compose, utilizza la seguente dipendenza nel file build.gradle del modulo dell'app:

Groovy

dependencies {
    def nav_version = "2.7.7"

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

Kotlin

dependencies {
    val nav_version = "2.7.7"

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

Inizia

Quando implementi la navigazione in un'app, implementa un host di navigazione, un grafico e un controller. Per ulteriori informazioni, consulta la panoramica di Navigazione.

Per informazioni su come creare un NavController in Compose, consulta la sezione Compose di Creare un controller di navigazione.

Crea un NavHost

Per informazioni su come creare un NavHost in Compose, consulta la sezione Scrivi di Progettare il grafico di navigazione.

Per informazioni su come raggiungere un elemento componibile, consulta Accedere a una destinazione nella documentazione sull'architettura.

Navigazione Compose supporta anche il passaggio di argomenti tra destinazioni componibili. A questo scopo, devi aggiungere segnaposto di argomenti al percorso, in modo simile a come aggiungi argomenti a un link diretto quando utilizzi la libreria di navigazione di base:

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

Per impostazione predefinita, tutti gli argomenti vengono analizzati come stringhe. Il parametro arguments di composable() accetta un elenco di oggetti NamedNavArgument. Puoi creare rapidamente un elemento NamedNavArgument utilizzando il metodo navArgument() e specificarne esattamente il valore type:

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

Devi estrarre gli argomenti dal file NavBackStackEntry che è disponibile nella funzione lambda della funzione composable().

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

Per passare l'argomento alla destinazione, devi aggiungerlo alla route quando effettui la chiamata navigate:

navController.navigate("profile/user1234")

Per un elenco dei tipi supportati, consulta l'articolo Passare dati tra destinazioni.

Recupera dati complessi durante la navigazione

È vivamente consigliato non trasferire oggetti di dati complessi durante la navigazione, ma passare il minimo di informazioni necessarie, come un identificatore univoco o un'altra forma di ID, come argomenti durante l'esecuzione di azioni di navigazione:

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

Gli oggetti complessi dovrebbero essere archiviati come dati in un'unica fonte attendibile, ad esempio il livello dati. Una volta giunti alla destinazione dopo la navigazione, puoi caricare le informazioni richieste dall'unica fonte attendibile utilizzando l'ID passato. Per recuperare gli argomenti in ViewModel responsabili dell'accesso al livello dati, utilizza SavedStateHandle di ViewModel:

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)

// …

}

Questo approccio aiuta a prevenire la perdita di dati durante le modifiche alla configurazione ed eventuali incoerenze quando l'oggetto in questione viene aggiornato o modificato.

Per una spiegazione più approfondita sul motivo per cui dovresti evitare di trasferire dati complessi sotto forma di argomenti, oltre a un elenco dei tipi di argomenti supportati, consulta la pagina Passare dati tra destinazioni.

Aggiungi argomenti facoltativi

Scrittura navigazione supporta anche argomenti di navigazione facoltativi. Gli argomenti facoltativi differiscono dagli argomenti richiesti per due aspetti:

  • Devono essere inclusi utilizzando la sintassi dei parametri di query ("?argName={argName}")
  • Devono avere un valore defaultValue impostato o avere nullable = true (che imposta implicitamente il valore predefinito su null)

Ciò significa che tutti gli argomenti facoltativi devono essere aggiunti esplicitamente alla funzione composable() come elenco:

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

Ora, anche se non sono stati passati argomenti alla destinazione, viene utilizzato l'oggetto defaultValue, "user1234".

La struttura della gestione degli argomenti attraverso le route significa che i componenti componibili rimangono completamente indipendenti dall'esplorazione e li rende molto più testabili.

Scrittura navigazione supporta i link diretti impliciti che possono essere definiti anche come parte della funzione composable(). Il suo parametro deepLinks accetta un elenco di oggetti NavDeepLink che possono essere creati rapidamente utilizzando il metodo navDeepLink():

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

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

Questi link diretti ti consentono di associare un URL, un'azione o un tipo MIME specifico a un componibile. Per impostazione predefinita, questi link diretti non sono esposti alle app esterne. Per rendere questi link diretti disponibili esternamente, devi aggiungere gli elementi <intent-filter> appropriati al file manifest.xml della tua app. Per attivare il link diretto nell'esempio precedente, devi aggiungere quanto segue all'interno dell'elemento <activity> del manifest:

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

La navigazione include automaticamente link diretti al componibile quando il link diretto viene attivato da un'altra app.

Gli stessi link diretti possono essere utilizzati anche per creare un PendingIntent con il link diretto appropriato di un componibile:

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)
}

Puoi quindi usare deepLinkPendingIntent come qualsiasi altro PendingIntent per aprire la tua app nella destinazione del link diretto.

Navigazione nidificata

Per informazioni su come creare grafici di navigazione nidificati, consulta Grafici nidificati.

Integrazione con la barra di navigazione in basso

Se definisci NavController a un livello superiore nella gerarchia componibile, puoi collegare il riquadro di navigazione ad altri componenti, ad esempio il componente di navigazione in basso. In questo modo puoi navigare selezionando le icone nella barra in basso.

Per utilizzare i componenti BottomNavigation e BottomNavigationItem, aggiungi la dipendenza androidx.compose.material all'applicazione Android.

Groovy

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

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.12"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

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

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.12"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Per collegare gli elementi in una barra di navigazione in basso alle route nel grafico di navigazione, è consigliabile definire una classe sealed, come Screen mostrata qui, che contiene il route e l'ID risorsa String per le destinazioni.

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)
}

Inserisci quindi questi elementi in un elenco che possa essere utilizzato da BottomNavigationItem:

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

Nel tuo componibile BottomNavigation, recupera l'attuale NavBackStackEntry utilizzando la funzione currentBackStackEntryAsState(). Questa voce ti consente di accedere all'attuale NavDestination. Lo stato selezionato di ogni BottomNavigationItem può quindi essere determinato confrontando la route dell'elemento con la route della destinazione corrente e delle sue destinazioni padre per gestire i casi quando utilizzi la navigazione nidificata usando la gerarchia NavDestination.

Il percorso dell'elemento viene utilizzato anche per collegare il lambda onClick a una chiamata a navigate, in modo che toccando l'elemento si acceda a quell'elemento. Se utilizzi i flag saveState e restoreState, lo stato e lo stack posteriore dell'elemento vengono salvati e ripristinati correttamente quando passi da un elemento di navigazione in basso.

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) }
  }
}

In questo caso, puoi utilizzare il metodo NavController.currentBackStackEntryAsState() per sollevare lo stato navController dalla funzione NavHost e condividerlo con il componente BottomNavigation. Ciò significa che BottomNavigation ha automaticamente lo stato più aggiornato.

Digita Sicurezza in Scrittura Navigazione

Il codice in questa pagina non è a prova di tipo. Puoi chiamare la funzione navigate() con route inesistenti o argomenti errati. Tuttavia, puoi strutturare il codice di navigazione in modo che sia sicuro per il tipo di runtime. In questo modo, puoi evitare arresti anomali e assicurarti che:

  • Gli argomenti forniti quando accedi a una destinazione o a un grafico di navigazione sono i tipi corretti e che sono presenti tutti gli argomenti richiesti.
  • Gli argomenti recuperati da SavedStateHandle sono i tipi corretti.

Per ulteriori informazioni, consulta Sicurezza dei caratteri in Kotlin DSL e Navigation Compose.

Interoperabilità

Se vuoi utilizzare il componente Navigazione con Scrivi, hai due opzioni:

  • Definisci un grafico di navigazione con il componente Navigazione per i frammenti.
  • Definisci un grafico di navigazione con NavHost in Compose utilizzando le destinazioni di Compose. Ciò è possibile solo se tutte le schermate nel grafico di navigazione sono componibili.

Di conseguenza, per le app miste Scrittura e Visualizzazioni è consigliabile utilizzare il componente di navigazione basato su frammenti. I frammenti conterranno schermate basate su visualizzazioni, schermate di composizione e schermate che usano sia Visualizzazioni che Compose. Una volta che i contenuti di ogni fragment sono in Compose, il passaggio successivo consiste nel unire tutte queste schermate con Compose Navigation e rimuovere tutti i frammenti.

Per modificare le destinazioni all'interno del codice di Compose, devi esporre gli eventi che possono essere trasmessi e attivati da qualsiasi componibile nella gerarchia:

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

Nel frammento, crei il ponte tra Compose e il componente di navigazione basato su frammenti individuando il NavController e passando alla destinazione:

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

In alternativa, puoi trasferire NavController alla gerarchia di Compose. Tuttavia, l'esposizione di funzioni semplici è molto più riutilizzabile e verificabile.

Test

Disaccoppia il codice di navigazione dalle destinazioni componibili per consentire di testare ogni componibile separatamente, separato dall'elemento componibile NavHost.

Ciò significa che non devi passare navController direttamente a nessun elemento componibile e passare invece i callback di navigazione come parametri. In questo modo tutti i tuoi componibili possono essere testati singolarmente, in quanto non richiedono un'istanza di navController nei test.

Il livello di indirezione fornito dalla funzione lambda composable è ciò che ti consente di separare il codice di navigazione dal codice componibile stesso. Questo funziona in due direzioni:

  • Passa solo gli argomenti analizzati nel tuo componibile
  • Inserisci i lambda che dovrebbero essere attivati dal componibile per la navigazione, anziché lo stesso NavController.

Ad esempio, un componibile Profile che accetta un userId come input e consente agli utenti di accedere alla pagina del profilo di un amico potrebbe avere la firma di:

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

In questo modo, il componibile Profile funziona in modo indipendente da Navigation, consentendo di testarlo in modo indipendente. La funzione lambda composable incapsulerebbe la logica minima necessaria per colmare il divario tra le API di navigazione e il componibile:

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

Ti consigliamo di scrivere test che coprano i requisiti di navigazione dell'app testando NavHost, le azioni di navigazione trasmesse ai componenti componibili e i singoli componenti componibili delle schermate.

Test di NavHost in corso...

Per iniziare a testare NavHost , aggiungi la seguente dipendenza per i test di navigazione:

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

Puoi configurare l'oggetto di test NavHost e passargli un'istanza dell'istanza navController. Per questo, l'artefatto del test di navigazione fornisce un elemento TestNavHostController. Un test dell'interfaccia utente che verifica la destinazione iniziale della tua app e di NavHost avrebbe il seguente aspetto:

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()
    }
}

Test delle azioni di navigazione

Puoi testare l'implementazione della navigazione in diversi modi facendo clic sugli elementi UI e quindi verificando la destinazione visualizzata o confrontando il percorso previsto con quello attuale.

Poiché vuoi testare l'implementazione concreta dell'app, sono preferibile i clic sull'UI. Per scoprire come eseguire il test insieme alle singole funzioni componibili isolate, consulta il codelab relativo ai test in Jetpack Compose.

Puoi anche utilizzare navController per verificare le tue asserzioni confrontando la route Stringa attuale con quella prevista, utilizzando currentBackStackEntry di navController:

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

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

Per ulteriori indicazioni sulle nozioni di base dei test di Compose, consulta Test del layout di Compose e il codelab su Test in Jetpack Compose. Per scoprire di più sul test avanzato del codice di navigazione, consulta la guida Navigazione di test.

Scopri di più

Per scoprire di più su Jetpack Navigation, consulta la guida introduttiva al componente Navigazione o esegui il codelab relativo alla navigazione Jetpack Compose.

Per scoprire come progettare la navigazione dell'app in modo che si adatti a dimensioni dello schermo, orientamenti e fattori di forma, consulta Navigazione per UI adattabili.

Per informazioni su un'implementazione più avanzata della navigazione in Compose in un'app modularizzata, inclusi concetti come grafici nidificati e integrazione della barra di navigazione in basso, dai un'occhiata all'app Ora in Android su GitHub.

Campioni