Navigation 3 introduit un système puissant et flexible pour gérer le flux d'UI de votre application via des scènes. Les scènes vous permettent de créer des mises en page hautement personnalisées, de vous adapter à différentes tailles d'écran et de gérer des expériences multi-volets complexes de manière transparente.
Comprendre les scènes
Dans Navigation 3, un Scene
est l'unité de base qui affiche une ou plusieurs instances NavEntry
. Considérez une Scene
comme un état visuel ou une section distincte de votre UI pouvant contenir et gérer l'affichage du contenu de votre pile "Retour".
Chaque instance Scene
est identifiée de manière unique par son key
et la classe de l'Scene
elle-même. Cet identifiant unique est crucial, car il pilote l'animation de niveau supérieur lorsque Scene
change.
L'interface Scene
présente les propriétés suivantes:
key: Any
: identifiant unique de cette instanceScene
spécifique. Cette clé, combinée à la classe deScene
, garantit la distinction, principalement à des fins d'animation.entries: List<NavEntry<T>>
: liste des objetsNavEntry
que leScene
est chargé d'afficher. Il est important de noter que si le mêmeNavEntry
s'affiche dans plusieursScenes
lors d'une transition (par exemple, dans une transition d'élément partagé), son contenu ne sera rendu que par laScene
cible la plus récente qui l'affiche.previousEntries: List<NavEntry<T>>
: cette propriété définit lesNavEntry
qui en résulteraient si une action "Retour" se produisait à partir de l'Scene
actuelle. Il est essentiel pour calculer l'état de retour prédictif approprié, ce qui permet àNavDisplay
d'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 comment leScene
affiche sonentries
et les éléments d'UI environnants spécifiques à ceScene
.
Comprendre les stratégies de scène
SceneStrategy
est le mécanisme qui détermine comment une liste donnée de NavEntry
s de la pile "Retour" doit être organisée et convertie en Scene
. Essentiellement, lorsqu'il est présenté avec les entrées de la pile "Retour" actuelles, un SceneStrategy
se pose deux questions clés:
- Puis-je créer un
Scene
à partir de ces entrées ? Si leSceneStrategy
détermine qu'il peut gérer lesNavEntry
donnés et former unScene
pertinent (par exemple, une boîte de dialogue ou une mise en page multi-volets), il continue. Sinon, il renvoienull
, ce qui permet aux autres stratégies de créer unScene
. - Si oui, comment dois-je organiser ces entrées dans le
Scene?
? Une fois qu'unSceneStrategy
s'engage à gérer les entrées, il se charge de créer unScene
et de définir la façon dont lesNavEntry
spécifiés s'affichent 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 utilise l'List<NavEntry<T>>
actuelle de la pile "Retour" et un rappel onBack
. Il doit renvoyer un Scene<T>
s'il peut en créer un à partir des entrées fournies, ou null
s'il ne peut pas.
SceneStrategy
fournit également une fonction 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 ne peut pas le faire, elle délègue la tâche à la suivante de la chaîne.
Fonctionnement des scènes et des stratégies de scène
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 aucun Scene
n'est calculé par la stratégie fournie (ou la chaîne de stratégies), NavDisplay
utilise automatiquement un SinglePaneSceneStrategy
par défaut.
Voici le détail de l'interaction:
- Lorsque vous ajoutez ou supprimez des touches de votre pile "Retour" (par exemple, à l'aide de
backStack.add()
oubackStack.removeLastOrNull()
),NavDisplay
observe ces modifications. NavDisplay
transmet la liste actuelle deNavEntrys
(dérivée des clés de pile arrière) à la méthodeSceneStrategy's calculateScene
configurée.- Si
SceneStrategy
renvoie unScene
,NavDisplay
affiche ensuite lecontent
de ceScene
.NavDisplay
gère également les animations et la prévisualisation du Retour 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 que vous puissiez avoir est un affichage à un seul volet, qui est le comportement par défaut si aucun autre SceneStrategy
n'a la priorité.
data class SinglePaneScene<T : Any>( override val key: T, 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.invoke(entry.key) } } /** * 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().key, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
Exemple: Mise en page à deux volets de base (scène et stratégie personnalisées)
Cet exemple montre comment créer une mise en page à deux volets simple qui est activée en fonction de deux conditions:
- La largeur de la fenêtre est suffisamment large 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é de 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.invoke(firstEntry.key) } Column(modifier = Modifier.weight(0.5f)) { secondEntry.content.invoke(secondEntry.key) } } } 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.key, secondEntry.key) 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 cette TwoPaneSceneStrategy
dans votre NavDisplay
, modifiez vos appels entryProvider
pour inclure des métadonnées TwoPaneScene.twoPane()
pour les entrées que vous souhaitez afficher dans une mise en page à deux volets. Fournissez ensuite TwoPaneSceneStrategy()
comme sceneStrategy
, en vous appuyant sur le remplacement 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/des détails dans une scène Material Adaptive
Pour le cas d'utilisation Liste/Détail, l'artefact androidx.compose.material3.adaptive:adaptive-navigation3
fournit un ListDetailSceneStrategy
qui crée un Scene
Liste/Détail. Cette Scene
gère automatiquement les dispositions multi-volets complexes (volets de liste, de détail et 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
de détails de la liste de matériaux, procédez comme suit:
- Ajoutez la dépendance: incluez
androidx.compose.material3.adaptive:adaptive-navigation3
dans le fichierbuild.gradle.kts
de votre projet. - Définissez vos entrées avec des métadonnées
ListDetailSceneStrategy
: utilisezlistPane(), detailPane()
etextraPane()
pour marquer votreNavEntrys
afin d'afficher le volet approprié. L'aidelistPane()
vous permet également de spécifier undetailPlaceholder
lorsqu'aucun élément n'est sélectionné. - Use
rememberListDetailSceneStrategy
(): cette fonction composable fournit unListDetailSceneStrategy
préconfiguré pouvant être utilisé par unNavDisplay
.
L'extrait de code suivant est un exemple de Activity
illustrant l'utilisation de 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") } } ) } } } }