Il componente Navigation fornisce supporto per le applicazioni Jetpack Compose. Puoi spostarti tra i composable sfruttando l'infrastruttura e le funzionalità del componente Navigation.
Per la libreria di navigazione alpha più recente creata appositamente per Compose, consulta la documentazione di Navigation 3.
Configura
Per supportare Compose, utilizza la seguente dipendenza nel file build.gradle
del modulo dell'app:
Groovy
dependencies { def nav_version = "2.9.1" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.1" 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 saperne di più, consulta la panoramica della navigazione.
Crea un NavController
Per informazioni su come creare un NavController
in Compose, consulta la sezione Compose
dell'articolo Creare un controller di navigazione.
Crea un NavHost
Per informazioni su come creare un NavHost
in Compose, consulta la sezione Compose
dell'articolo Progettare il grafico di navigazione.
Andare a un elemento componibile
Per informazioni su come navigare in un componente componibile, consulta Navigare verso una destinazione nella documentazione sull'architettura.
Navigare con gli argomenti
Per informazioni sul passaggio di argomenti tra destinazioni componibili, consulta la sezione Compose di Progettare il grafico di navigazione.
Recuperare dati complessi durante la navigazione
È consigliabile non passare oggetti dati complessi durante la navigazione, ma passare invece le informazioni minime necessarie, come un identificatore univoco o un altro tipo 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(id = "user1234"))
Gli oggetti complessi devono essere archiviati come dati in un'unica fonte attendibile, ad esempio
il data layer. Una volta arrivato a destinazione dopo la navigazione, puoi caricare le informazioni richieste dalla singola fonte attendibile utilizzando l'ID trasmesso. Per recuperare gli argomenti nel ViewModel
responsabile dell'accesso al data layer, utilizza SavedStateHandle
di ViewModel
:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// …
}
Questo approccio contribuisce a evitare la perdita di dati durante le modifiche alla configurazione e qualsiasi incoerenza quando l'oggetto in questione viene aggiornato o modificato.
Per una spiegazione più approfondita del motivo per cui devi evitare di passare dati complessi come argomenti, nonché un elenco dei tipi di argomenti supportati, vedi Trasferire dati tra destinazioni.
Link diretti
Navigation Compose supporta anche i link diretti che possono essere definiti come parte
della funzione composable()
. Il parametro deepLinks
accetta un elenco di oggetti
NavDeepLink
che possono essere creati rapidamente utilizzando il metodo
navDeepLink()
:
@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "$uri/profile")
)
) { backStackEntry ->
ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}
Questi link diretti ti consentono di associare un URL, un'azione o un tipo MIME specifico a un
componente componibile. Per impostazione predefinita, questi link diretti non sono esposti ad app esterne. Per
rendere disponibili esternamente questi link diretti, 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 crea automaticamente un link diretto a questo elemento componibile quando il link diretto viene attivato da un'altra app.
Questi stessi link diretti possono essere utilizzati anche per creare un PendingIntent
con il
link diretto appropriato da un elemento componibile:
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/profile/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
A questo punto, puoi utilizzare questo deepLinkPendingIntent
come qualsiasi altro PendingIntent
per
aprire la tua app nella destinazione del link diretto.
Navigazione annidata
Per informazioni su come creare grafici di navigazione nidificati, consulta Grafici nidificati.
Integrazione con la barra di navigazione inferiore
Definendo NavController
a un livello superiore della gerarchia componibile,
puoi connettere la navigazione ad altri componenti come il componente di navigazione inferiore. 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
alla tua applicazione Android.
Groovy
dependencies { implementation "androidx.compose.material:material:1.8.3" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.8.3") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Per collegare gli elementi di una barra di navigazione inferiore ai percorsi nel grafico di navigazione,
ti consigliamo di definire una classe, ad esempio TopLevelRoute
mostrata qui, che abbia
una classe di percorso e un'icona.
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
Quindi, inserisci queste route in un elenco che può essere utilizzato da
BottomNavigationItem
:
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
Nel composable BottomNavigation
, ottieni l'attuale NavBackStackEntry
utilizzando la funzione currentBackStackEntryAsState()
. Questa voce ti consente di accedere
all'attuale NavDestination
. Lo stato selezionato di ogni
BottomNavigationItem
può essere determinato confrontando il percorso dell'elemento con
il percorso della destinazione corrente e delle relative destinazioni principali per gestire i casi
in cui utilizzi la navigazione nidificata utilizzando la gerarchia
NavDestination
.
Il percorso dell'elemento viene utilizzato anche per connettere la lambda onClick
a una chiamata a
navigate
in modo che toccando l'elemento si passi a quell'elemento. Utilizzando i flag
saveState
e restoreState
, lo stato e lo stack precedente dell'elemento vengono
salvati e ripristinati correttamente quando passi da un elemento all'altro della barra di navigazione inferiore.
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
topLevelRoutes.forEach { topLevelRoute ->
BottomNavigationItem(
icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
label = { Text(topLevelRoute.name) },
selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
onClick = {
navController.navigate(topLevelRoute.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 = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
Qui sfrutti il metodo NavController.currentBackStackEntryAsState()
per estrarre lo stato navController
dalla funzione NavHost
e condividerlo con il componente BottomNavigation
. Ciò significa che BottomNavigation
ha automaticamente lo stato più aggiornato.
Interoperabilità
Se vuoi utilizzare il componente Navigation con Compose, hai due opzioni:
- Definisci un grafico di navigazione con il componente Navigation per i fragment.
- Definisci un grafico di navigazione con un
NavHost
in Compose utilizzando le destinazioni Compose. Ciò è possibile solo se tutti gli schermi nel grafico di navigazione sono componibili.
Pertanto, il consiglio per le app che utilizzano sia Compose che Views è di utilizzare il componente di navigazione basato su fragment. I fragment conterranno quindi schermate basate su View, schermate di composizione e schermate che utilizzano sia View che Compose. Una volta che i contenuti di ogni fragment sono in Compose, il passaggio successivo consiste nel collegare tutte queste schermate con Navigation Compose e rimuovere tutti i fragment.
Navigare da Compose con Navigation per i fragment
Per modificare le destinazioni all'interno di Compose code, esponi gli eventi che possono essere passati e attivati da qualsiasi elemento componibile nella gerarchia:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
Nel fragment, crei il ponte tra Compose e il componente di navigazione basato sui fragment trovando NavController
e navigando verso la destinazione:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
In alternativa, puoi passare NavController
nella gerarchia di Compose.
Tuttavia, l'esposizione di funzioni semplici è molto più riutilizzabile e testabile.
Test
Disaccoppia il codice di navigazione dalle destinazioni componibili per consentire il test
di ogni componente componibile in modo isolato, separato dal componente componibile NavHost
.
Ciò significa che non devi passare navController
direttamente in qualsiasi
componente e devi invece passare i callback di navigazione come parametri. In questo modo, tutti i tuoi composable possono essere testati singolarmente, in quanto non richiedono un'istanza di navController
nei test.
Il livello di indirezione fornito dalla lambda composable
consente di
separare il codice di navigazione dal composable stesso. Funziona in due
direzioni:
- Passa solo gli argomenti analizzati nel tuo componibile
- Passa le espressioni lambda che devono essere attivate dal componibile per navigare, anziché
NavController
stesso.
Ad esempio, un composable ProfileScreen
che accetta un userId
come input e
consente agli utenti di passare alla pagina del profilo di un amico potrebbe avere la seguente firma:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
In questo modo, il componente componibile ProfileScreen
funziona indipendentemente dalla navigazione,
consentendo di testarlo separatamente. La lambda composable
incapsulerebbe la logica minima necessaria per colmare il divario tra le API Navigation
e il tuo composable:
@Serializable data class Profile(id: String)
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
È consigliabile scrivere test che coprano i requisiti di navigazione dell'app testando
NavHost
, le azioni di navigazione trasmesse ai composable e
i singoli composable delle schermate.
Test del NavHost
Per iniziare a testare NavHost
, aggiungi la seguente dipendenza di test di navigazione:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Inserisci NavHost
della tua app in un componente componibile che accetta NavHostController
come
parametro.
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
Ora puoi testare AppNavHost
e tutta la logica di navigazione definita all'interno di
NavHost
passando un'istanza dell'artefatto di test di navigazione
TestNavHostController
. Un test UI che verifica la destinazione iniziale della tua app e NavHost
avrebbe questo 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 dell'interfaccia utente e verificando la destinazione visualizzata o confrontando il percorso previsto con quello attuale.
Poiché vuoi testare l'implementazione della tua app concreta, sono preferibili i clic sull'interfaccia utente. Per scoprire come testare questo elemento insieme alle singole funzioni componibili in isolamento, assicurati di consultare il codelab Test in Jetpack Compose.
Puoi anche utilizzare navController
per controllare le tue asserzioni confrontando il percorso attuale con quello previsto, utilizzando navController
's
currentBackStackEntry
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
Per ulteriori indicazioni sulle nozioni di base dei test di Compose, consulta Testare il layout di Compose e il codelab Test in Jetpack Compose. Per scoprire di più sui test avanzati del codice di navigazione, consulta la guida Test di navigazione.
Scopri di più
Per scoprire di più su Jetpack Navigation, consulta la pagina Guida introduttiva al componente Navigation o segui il codelab di Jetpack Compose Navigation.
Per scoprire come progettare la navigazione della tua app in modo che si adatti a diverse dimensioni, orientamenti e fattori di forma dello schermo, consulta Navigazione per UI adattabili.
Per scoprire di più su un'implementazione più avanzata della navigazione di Compose in un'app modularizzata, inclusi concetti come grafici nidificati e integrazione della barra di navigazione inferiore, dai un'occhiata all'app Now in Android su GitHub.
Campioni
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Material Design 2 in Compose
- Migrare Jetpack Navigation a Navigation Compose
- Dove eseguire l'hoisting dello stato