Navigation 3 introduit un système puissant et flexible pour gérer le flux d'UI de votre application à l'aide des scènes. Les scènes vous permettent de créer des mises en page très personnalisées, de vous adapter à différentes tailles d'écran et de gérer des expériences complexes à plusieurs volets de manière fluide.
Comprendre les scènes
Dans Navigation 3, un Scene est l'unité fondamentale qui affiche une ou plusieurs instances NavEntry. Considérez une Scene comme un état visuel ou une section distincte de votre UI, qui peut contenir et gérer l'affichage du contenu de votre backend.
Chaque instance Scene est identifiée de manière unique par son key et la classe du Scene lui-même. Cet identifiant unique est essentiel, car il pilote l'animation de premier niveau lorsque Scene change.
L'interface Scene présente les propriétés suivantes :
key: Any: identifiant unique de cette instanceScenespécifique. Cette clé, combinée à la classe deScene, garantit la distinction, principalement à des fins d'animation.entries: List<NavEntry<T>>: liste des objetsNavEntryque leSceneest chargé d'afficher. Il est important de noter que si le mêmeNavEntryest affiché dans plusieursSceneslors d'une transition (par exemple, dans une transition d'élément partagé), son contenu ne sera rendu que par leScenecible le plus récent qui l'affiche.previousEntries: List<NavEntry<T>>: cette propriété définit lesNavEntryqui résulteraient d'une action "Retour" à partir duSceneactuel. Il est essentiel pour calculer l'état de retour prédictif approprié, ce qui permet àNavDisplayd'anticiper et de passer à l'état précédent correct, qui peut être une scène avec une classe et/ou une clé différentes.content: @Composable () -> Unit: il s'agit de la fonction composable dans laquelle vous définissez la façon dontSceneaffiche sonentrieset tous les éléments d'UI environnants spécifiques à ceScene.
Comprendre les stratégies de scène
Un SceneStrategy est le mécanisme qui détermine comment une liste donnée de NavEntry de la pile de retour doit être organisée et transformée en Scene. En substance, lorsqu'un SceneStrategy est présenté avec les entrées de pile arrière actuelles, il se pose deux questions clés :
- Puis-je créer un
Sceneà partir de ces entrées ? Si leSceneStrategydétermine qu'il peut gérer lesNavEntryet former unScenesignificatif (par exemple, une boîte de dialogue ou une mise en page à plusieurs volets), il poursuit le processus. Sinon, elle renvoienull, ce qui permet à d'autres stratégies de créer unScene. - Si c'est le cas, comment dois-je organiser ces entrées dans
Scene?? Une fois qu'unSceneStrategys'engage à gérer les entrées, il assume la responsabilité de construire unSceneet de définir la façon dont lesNavEntryspécifiés seront affichés dans ceScene.
Le cœur d'un SceneStrategy est sa méthode calculateScene :
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
Cette méthode est une fonction d'extension sur un SceneStrategyScope qui prend le List<NavEntry<T>> actuel de la pile de retour. Elle doit renvoyer un Scene<T> si elle peut en former un à partir des entrées fournies, ou null si elle ne le peut pas.
Le SceneStrategyScope est responsable de la gestion des arguments facultatifs dont le SceneStrategy peut avoir besoin, comme un rappel onBack.
SceneStrategy fournit également une fonction d'infixe then pratique, qui vous permet d'enchaîner plusieurs stratégies. Cela crée un pipeline de prise de décision flexible dans lequel chaque stratégie peut tenter de calculer un Scene. Si elle n'y parvient pas, elle délègue la tâche à la stratégie suivante de la chaîne.
Comment les scènes et les stratégies de scène fonctionnent-elles ensemble ?
NavDisplay est le composable central qui observe votre pile "Retour" et utilise un SceneStrategy pour déterminer et afficher le Scene approprié.
Le paramètre NavDisplay's sceneStrategy attend un SceneStrategy chargé de calculer le Scene à afficher. Si aucune Scene n'est calculée par la stratégie (ou la chaîne de stratégies) fournie, NavDisplay revient automatiquement à l'utilisation d'une SinglePaneSceneStrategy par défaut.
Voici le détail de l'interaction :
- Lorsque vous ajoutez ou supprimez des clés de votre pile de retour (par exemple, à l'aide de
backStack.add()oubackStack.removeLastOrNull()),NavDisplayobserve ces modifications. NavDisplaytransmet la liste actuelle desNavEntrys(dérivée des clés de la pile de retour) à la méthodeSceneStrategy's calculateSceneconfigurée.- Si
SceneStrategyrenvoie unScene,NavDisplayaffiche lecontentde ceScene.NavDisplaygère également les animations et la prévisualisation du retour en arrière en fonction des propriétés deScene.
Exemple : Mise en page à volet unique (comportement par défaut)
La mise en page personnalisée la plus simple est un affichage à un seul volet, qui est le comportement par défaut si aucune autre SceneStrategy n'est prioritaire.
data class SinglePaneScene<T : Any>( override val key: Any, val entry: NavEntry<T>, override val previousEntries: List<NavEntry<T>>, ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(entry) override val content: @Composable () -> Unit = { entry.Content() } } /** * A [SceneStrategy] that always creates a 1-entry [Scene] simply displaying the last entry in the * list. */ public class SinglePaneSceneStrategy<T : Any> : SceneStrategy<T> { @Composable override fun calculateScene(entries: List<NavEntry<T>>, onBack: (Int) -> Unit): Scene<T> = SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
Exemple : Mise en page de base à deux volets (Scène et stratégie personnalisées)
Cet exemple montre comment créer une mise en page simple à deux volets qui est activée en fonction de deux conditions :
- La largeur de la fenêtre est suffisamment grande pour prendre en charge deux volets (c'est-à-dire au moins
WIDTH_DP_MEDIUM_LOWER_BOUND). - Les deux premières entrées de la pile "Retour" déclarent explicitement leur compatibilité avec l'affichage dans une mise en page à deux volets à l'aide de métadonnées spécifiques.
L'extrait de code suivant est le code source combiné pour TwoPaneScene.kt et TwoPaneSceneStrategy.kt :
// --- TwoPaneScene --- /** * A custom [Scene] that displays two [NavEntry]s side-by-side in a 50/50 split. */ class TwoPaneScene<T : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, val firstEntry: NavEntry<T>, val secondEntry: NavEntry<T> ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(firstEntry, secondEntry) override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.5f)) { firstEntry.Content() } Column(modifier = Modifier.weight(0.5f)) { secondEntry.Content() } } } companion object { internal const val TWO_PANE_KEY = "TwoPane" /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * in a two-pane layout. */ fun twoPane() = mapOf(TWO_PANE_KEY to true) } } // --- TwoPaneSceneStrategy --- /** * A [SceneStrategy] that activates a [TwoPaneScene] if the window is wide enough * and the top two back stack entries declare support for two-pane display. */ class TwoPaneSceneStrategy<T : Any> : SceneStrategy<T> { @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3WindowSizeClassApi::class) @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass // Condition 1: Only return a Scene if the window is sufficiently wide to render two panes. // We use isWidthAtLeastBreakpoint with WIDTH_DP_MEDIUM_LOWER_BOUND (600dp). if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { return null } val lastTwoEntries = entries.takeLast(2) // Condition 2: Only return a Scene if there are two entries, and both have declared // they can be displayed in a two pane scene. return if (lastTwoEntries.size == 2 && lastTwoEntries.all { it.metadata.containsKey(TwoPaneScene.TWO_PANE_KEY) } ) { val firstEntry = lastTwoEntries.first() val secondEntry = lastTwoEntries.last() // The scene key must uniquely represent the state of the scene. val sceneKey = Pair(firstEntry.contentKey, secondEntry.contentKey) TwoPaneScene( key = sceneKey, // Where we go back to is a UX decision. In this case, we only remove the top // entry from the back stack, despite displaying two entries in this scene. // This is because in this app we only ever add one entry to the // back stack at a time. It would therefore be confusing to the user to add one // when navigating forward, but remove two when navigating back. previousEntries = entries.dropLast(1), firstEntry = firstEntry, secondEntry = secondEntry ) } else { null } } }
Pour utiliser ce TwoPaneSceneStrategy dans votre NavDisplay, modifiez vos appels entryProvider afin d'inclure les métadonnées TwoPaneScene.twoPane() pour les entrées que vous souhaitez afficher dans une mise en page à deux volets. Ensuite, indiquez TwoPaneSceneStrategy() comme sceneStrategy, en vous appuyant sur la solution de repli par défaut pour les scénarios à un seul volet :
// Define your navigation keys @Serializable data object ProductList : NavKey @Serializable data class ProductDetail(val id: String) : NavKey @Composable fun MyAppContent() { val backStack = rememberNavBackStack(ProductList) NavDisplay( backStack = backStack, entryProvider = entryProvider { entry<ProductList>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Column { Text("Product List") Button(onClick = { backStack.add(ProductDetail("ABC")) }) { Text("View Details for ABC (Two-Pane Eligible)") } } } entry<ProductDetail>( // Mark this entry as eligible for two-pane display metadata = TwoPaneScene.twoPane() ) { key -> Text("Product Detail: ${key.id} (Two-Pane Eligible)") } // ... other entries ... }, // Simply provide your custom strategy. NavDisplay will fall back to SinglePaneSceneStrategy automatically. sceneStrategy = TwoPaneSceneStrategy<Any>(), onBack = { count -> repeat(count) { if (backStack.isNotEmpty()) { backStack.removeLastOrNull() } } } ) }
Afficher le contenu de la liste et des détails dans une scène adaptative Material
Pour le cas d'utilisation Liste/Détails, l'artefact androidx.compose.material3.adaptive:adaptive-navigation3 fournit un ListDetailSceneStrategy qui crée un Scene Liste/Détails. Ce Scene gère automatiquement les agencements complexes à plusieurs volets (listes, détails et volets supplémentaires) et les adapte en fonction de la taille de la fenêtre et de l'état de l'appareil.
Pour créer un Scene Material list-detail :
- Ajoutez la dépendance : incluez
androidx.compose.material3.adaptive:adaptive-navigation3dans le fichierbuild.gradle.ktsde votre projet. - Définissez vos entrées avec des métadonnées
ListDetailSceneStrategy: utilisezlistPane(), detailPane()etextraPane()pour marquer votreNavEntrysafin qu'il s'affiche dans le volet approprié. L'assistantlistPane()vous permet également de spécifier undetailPlaceholderlorsqu'aucun élément n'est sélectionné. - Utiliser
rememberListDetailSceneStrategy(): cette fonction composable fournit unListDetailSceneStrategypréconfiguré qui peut être utilisé par unNavDisplay.
L'extrait suivant est un exemple de Activity qui montre comment utiliser ListDetailSceneStrategy :
@Serializable object ProductList : NavKey @Serializable data class ProductDetail(val id: String) : NavKey @Serializable data object Profile : NavKey class MaterialListDetailActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3AdaptiveApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Scaffold { paddingValues -> val backStack = rememberNavBackStack(ProductList) val listDetailStrategy = rememberListDetailSceneStrategy<Any>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { keysToRemove -> repeat(keysToRemove) { backStack.removeLastOrNull() } }, sceneStrategy = listDetailStrategy, entryProvider = entryProvider { entry<ProductList>( metadata = ListDetailSceneStrategy.listPane( detailPlaceholder = { ContentYellow("Choose a product from the list") } ) ) { ContentRed("Welcome to Nav3") { Button(onClick = { backStack.add(ProductDetail("ABC")) }) { Text("View product") } } } entry<ProductDetail>( metadata = ListDetailSceneStrategy.detailPane() ) { product -> ContentBlue("Product ${product.id} ", Modifier.background(PastelBlue)) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Button(onClick = { backStack.add(Profile) }) { Text("View profile") } } } } entry<Profile>( metadata = ListDetailSceneStrategy.extraPane() ) { ContentGreen("Profile") } } ) } } } }