ב-Navigation 3 מוצגת מערכת חזקה וגמישה לניהול התהליך של ממשק המשתמש באפליקציה באמצעות סצנות. בעזרת סצנות אפשר ליצור פריסות בהתאמה אישית גבוהה, להתאים את התצוגה לגדלים שונים של מסכים ולנהל בקלות חוויות מורכבות עם כמה חלונות.
הסבר על סצנות
ב-Navigation 3, Scene
היא היחידה הבסיסית שמרינדרת מופע אחד או יותר של NavEntry
. אפשר לחשוב על Scene
כמצב חזותי או קטע ספציפי בממשק המשתמש, שיכול להכיל ולנהל את הצגת התוכן מהמקבץ האחורי.
כל מכונה של Scene
מזוהה באופן ייחודי באמצעות key
שלה והכיתה של ה-Scene
עצמו. המזהה הייחודי הזה חיוני כי הוא מניע את האנימציה ברמה העליונה כשהערך של Scene
משתנה.
לממשק Scene
יש את המאפיינים הבאים:
key: Any
: מזהה ייחודי של מופעScene
הספציפי הזה. המפתח הזה, בשילוב עם הכיתה שלScene
, מבטיח את הייחודיות, בעיקר למטרות אנימציה.entries: List<NavEntry<T>>
: זוהי רשימה של אובייקטים מסוגNavEntry
שה-Scene
אחראי להציג. חשוב לדעת: אם אותוNavEntry
מוצג בכמהScenes
במהלך מעבר (למשל, במעבר של רכיב משותף), התוכן שלו יומר רק על ידי היעדScene
האחרון שמוצג בו.previousEntries: List<NavEntry<T>>
: המאפיין הזה מגדיר את הערכים שלNavEntry
שיגרמו אם תתבצע פעולת 'חזרה' מה-Scene
הנוכחי. הוא חיוני לחישוב המצב החזוי הנכון של החזרה אחורה, ומאפשר ל-NavDisplay
לחזות את המצב הקודם הנכון ולעבור אליו, שיכול להיות סצנה עם מפתח או סיווג שונים.content: @Composable () -> Unit
: זוהי הפונקציה הניתנת ליצירה, שבה מגדירים איךScene
ידפיס אתentries
ואת כל רכיבי ממשק המשתמש שמסביב שספציפיים ל-Scene
הזה.
הסבר על אסטרטגיות סצנה
SceneStrategy
הוא המנגנון שקובע איך צריך לארגן רשימה נתונה של NavEntry
s מסטאק העורפי ולהעביר אותה ל-Scene
. בעיקרון, כשמוצגים לו הרשומות הנוכחיות ב-back stack, SceneStrategy
שואל את עצמו שתי שאלות מפתח:
- האם אפשר ליצור
Scene
מהרשומות האלה? אם ה-SceneStrategy
קובע שהוא יכול לטפל ב-NavEntry
s הנתונים וליצורScene
בעל משמעות (למשל, תיבת דו-שיח או פריסה עם כמה חלונות), הוא ממשיך. אחרת, הוא מחזיר את הערךnull
, ומאפשר לשיטות אחרות ליצורScene
. - אם כן, איך צריך לסדר את הרשומות האלה ב-
Scene?
אחרי ש-SceneStrategy
מתחייב לטפל ברשומות, הוא מקבל על עצמו את האחריות ליצורScene
ולהגדיר איךNavEntry
s שצוינו יוצגו ב-Scene
הזה.
הליבה של SceneStrategy
היא שיטת calculateScene
:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
השיטה הזו מקבלת את List<NavEntry<T>>
הנוכחי מסטאק העורפי ופונקציית קריאה חוזרת onBack
. הפונקציה אמורה להחזיר Scene<T>
אם היא יכולה ליצור קבוצה אחת מהרשומות שסופקו, או null
אם היא לא יכולה.
SceneStrategy
מספק גם פונקציית infix נוחה של then
, שמאפשרת לשרשר כמה אסטרטגיות יחד. כך נוצר צינור גמיש לקבלת החלטות, שבו כל אסטרטגיה יכולה לנסות לחשב את הערך של Scene
, ואם היא לא מצליחה, היא מעבירה את הבעיה לאסטרטגיה הבאה בשרשרת.
איך סצנות ואסטרטגיות סצנות פועלות יחד
NavDisplay
הוא הרכיב המרכזי ב-Composable, שמנטר את סטאק הקוד הקודם ומשתמש ב-SceneStrategy
כדי לקבוע ולייצר את Scene
המתאים.
הפרמטר NavDisplay's sceneStrategy
מצפה ל-SceneStrategy
שאחראי לחישוב הערך של Scene
להצגה. אם לא מתבצע חישוב של Scene
לפי האסטרטגיה שצוינה (או לפי שרשרת האסטרטגיות), NavDisplay
חוזר אוטומטית לשימוש ב-SinglePaneSceneStrategy
כברירת מחדל.
זהו פירוט האינטראקציה:
- כשמוסיפים או מסירים מפתחות מהמקבץ הקודם (למשל, באמצעות
backStack.add()
אוbackStack.removeLastOrNull()
), השינויים האלה מתועדים ב-NavDisplay
. - הפונקציה
NavDisplay
מעבירה את הרשימה הנוכחית שלNavEntrys
(שנגזרת ממפתחות ה-stack האחורי) לשיטהSceneStrategy's calculateScene
שהוגדרה. - אם הפונקציה
SceneStrategy
מחזירהScene
, ה-NavDisplay
ירנדר את ה-content
של ה-Scene
הזה. ה-NavDisplay
מנהל גם אנימציות וחזרה חזויית אחורה על סמך המאפיינים שלScene
.
דוגמה: פריסת חלונית אחת (התנהגות ברירת המחדל)
הפריסה המותאמת אישית הפשוטה ביותר היא תצוגה בחלונית אחת, וזוהי ברירת המחדל אם לא הוגדרה עדיפות ל-SceneStrategy
אחר.
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) ) }
דוגמה: פריסה בסיסית של שני חלונות (סצנה ואסטרטגיה בהתאמה אישית)
בדוגמה הזו מוסבר איך ליצור פריסה פשוטה של שני חלונות, שמופעל בהתאם לשני תנאים:
- רוחב החלון מספיק כדי לתמוך בשתי חלוניות (כלומר, לפחות
WIDTH_DP_MEDIUM_LOWER_BOUND
). - שתי הרשומות העליונות בסטאק העורפי מצהירות באופן מפורש על תמיכה בתצוגה של פריטים בשני חלונות באמצעות מטא-נתונים ספציפיים.
קטע הקוד הבא הוא קוד המקור המשולב של TwoPaneScene.kt
ו-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 } } }
כדי להשתמש ב-TwoPaneSceneStrategy
ב-NavDisplay
, משנים את הקריאות ל-entryProvider
כך שיכללו את המטא-נתונים של TwoPaneScene.twoPane()
עבור הרשומות שאתם רוצים להציג בפריסה של שתי חלוניות. לאחר מכן, מציינים את TwoPaneSceneStrategy()
כ-sceneStrategy
, ומסתמכים על ברירת המחדל החלופית בתרחישים של חלון יחיד:
// 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() } } } ) }
הצגת תוכן של רשימת פרטים בסצנה דינמית של Material
בתרחיש לדוגמה של פרטי רשימה, הארטיפקט androidx.compose.material3.adaptive:adaptive-navigation3
מספק ListDetailSceneStrategy
שיוצר Scene
של פרטי רשימה. ה-Scene
מטפל באופן אוטומטי בפריסות מורכבות עם כמה חלוניות (רשימת פריטים, חלונית פרטים וחלוניות נוספות) ומתאים אותן בהתאם לגודל החלון ולמצב המכשיר.
כדי ליצור פריט של רשימת חומרים Scene
:
- מוסיפים את התלות: כוללים את
androidx.compose.material3.adaptive:adaptive-navigation3
בקובץbuild.gradle.kts
של הפרויקט. - הגדרת הרשומות באמצעות המטא-נתונים של
ListDetailSceneStrategy
: משתמשים ב-listPane(), detailPane()
וב-extraPane()
כדי לסמן את ה-NavEntrys
להצגה בחלונית המתאימה. בעזרת הפונקציהlistPane()
אפשר גם לצייןdetailPlaceholder
כשלא נבחר פריט. - שימוש בפונקציה
rememberListDetailSceneStrategy
(): הפונקציה הניתנת ליצירה מחדש מספקתListDetailSceneStrategy
שהוגדר מראש, שאפשר להשתמש בו ב-NavDisplay
.
קטע הקוד הבא הוא קובץ Activity
לדוגמה שמראה את השימוש ב-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") } } ) } } } }