Navigation 3 מציג מערכת עוצמתית וגמישה לניהול זרימת ממשק המשתמש של האפליקציה באמצעות Scenes. סצנות מאפשרות ליצור פריסות מותאמות אישית, להתאים את הפריסות לגדלים שונים של מסכים ולנהל בצורה חלקה חוויות מורכבות עם חלונות מרובים.
הסבר על סצנות
בגרסה 3 של Navigation, Scene
היא היחידה הבסיסית שמציגה מופע אחד או יותר של NavEntry
. אפשר לחשוב על Scene
כמצב חזותי או כקטע נפרד בממשק המשתמש, שיכול להכיל תוכן מה-back stack ולהציג אותו.
כל מופע Scene
מזוהה באופן ייחודי על ידי key
והסיווג של Scene
עצמו. המזהה הייחודי הזה חשוב מאוד כי הוא מפעיל את האנימציה ברמה העליונה כשערך המאפיין Scene
משתנה.
לממשק Scene
יש את המאפיינים הבאים:
-
key: Any
: מזהה ייחודי של מופעScene
ספציפי. המפתח הזה, בשילוב עם המחלקה שלScene
, מבטיח ייחודיות, בעיקר למטרות אנימציה. -
entries: List<NavEntry<T>>
: רשימה של אובייקטים מסוגNavEntry
שאובייקטScene
אחראי להצגתם. חשוב לדעת: אם אותוNavEntry
מוצג בכמהScenes
במהלך מעבר (למשל, במעבר של רכיב משותף), התוכן שלו יעבור עיבוד רק על ידיScene
היעד האחרון שמציג אותו. -
previousEntries: List<NavEntry<T>>
: המאפיין הזה מגדיר אתNavEntry
s שיוצגו אם תתבצע פעולת 'חזרה' מה-Scene
הנוכחי. הוא חיוני לחישוב מצב החזרה החזוי הנכון, ומאפשר ל-NavDisplay
לצפות את המצב הקודם הנכון ולעבור אליו. המצב הקודם יכול להיות Scene עם מחלקה ו/או מפתח שונים. -
content: @Composable () -> Unit
: זוהי פונקציה שאפשר להרכיב ממנה פונקציות אחרות, שבה מגדירים איך הרכיבScene
מעבד את הרכיבentries
ואת כל רכיבי ממשק המשתמש שמסביב שספציפיים לרכיבScene
.
הסבר על אסטרטגיות של סצנות
SceneStrategy
הוא המנגנון שקובע איך רשימה נתונה של NavEntry
מהמחסנית האחורית צריכה להיות מסודרת ואיך היא צריכה לעבור לScene
. בעצם, כשמוצגים ל-SceneStrategy
הערכים הנוכחיים במחסנית החזרה, הוא שואל את עצמו שתי שאלות מרכזיות:
- האם אפשר ליצור
Scene
מהערכים האלה? אםSceneStrategy
קובע שהוא יכול לטפל בNavEntry
הנתון וליצורScene
משמעותי (למשל, תיבת דו-שיח או פריסה מרובת חלוניות), הוא ממשיך. אחרת, היא מחזירה את הערךnull
, כדי לאפשר לשיטות אחרות ליצורScene
. - אם כן, איך צריך לארגן את הרשומות האלה ב-
Scene?
אחרי ש-SceneStrategy
מתחייב לטפל ברשומות, הוא לוקח על עצמו את האחריות ליצירתScene
ולהגדרת האופן שבוNavEntry
s שצוינו יוצגו ב-Scene
הזה.
הליבה של SceneStrategy
היא ה-method calculateScene
:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
השיטה הזו היא פונקציית הרחבה ב-SceneStrategyScope
שמקבלת את List<NavEntry<T>>
הנוכחי ממחסנית החזרה. הפונקציה צריכה להחזיר Scene<T>
אם היא יכולה ליצור רשימה מהערכים שסופקו, או null
אם היא לא יכולה.
האובייקט SceneStrategyScope
אחראי לתחזוקה של כל הארגומנטים האופציונליים שאובייקט SceneStrategy
עשוי להזדקק להם, כמו onBack
callback.
SceneStrategy
מספקת גם פונקציית infix נוחה then
, שמאפשרת לכם לשרשר כמה אסטרטגיות יחד. הפעולה הזו יוצרת צינור עיבוד נתונים גמיש לקבלת החלטות, שבו כל שיטה מנסה לחשב Scene
, ואם היא לא מצליחה, היא מעבירה את ההחלטה לשיטה הבאה בשרשרת.
איך משלבים בין סצנות לבין אסטרטגיות של סצנות
NavDisplay
הוא רכיב מרכזי שניתן להרכבה, שעוקב אחרי ערימת הפעולות הקודמות ומשתמש ב-SceneStrategy
כדי לקבוע ולעבד את ה-Scene
המתאים.
הפרמטר NavDisplay's sceneStrategy
מצפה ל-SceneStrategy
שאחראי לחישוב של Scene
שיוצג. אם לא מחושב Scene
על ידי השיטה (או שרשרת השיטות) שצוינה, המערכת חוזרת אוטומטית לשימוש בSinglePaneSceneStrategy
כברירת מחדל.NavDisplay
פירוט האינטראקציה:
- כשמוסיפים או מסירים מקשים ממערך החזרה (לדוגמה, באמצעות
backStack.add()
אוbackStack.removeLastOrNull()
),NavDisplay
עוקב אחרי השינויים האלה. - ה-
NavDisplay
מעביר את הרשימה הנוכחית שלNavEntrys
(שנגזרת ממקשי ה-backstack) אל ה-methodSceneStrategy's calculateScene
שהוגדר. - אם הפונקציה
SceneStrategy
מחזירה בהצלחהScene
, הפונקציהNavDisplay
מעבדת אתcontent
של אותוScene
. בנוסף,NavDisplay
מנהל את האנימציות ואת התכונה 'חזרה עם חיזוי' על סמך המאפיינים שלScene
.
דוגמה: פריסה של חלונית יחידה (התנהגות ברירת המחדל)
הפריסה המותאמת אישית הכי פשוטה היא תצוגה עם חלונית אחת, שמוגדרת כברירת מחדל אם לא מוגדרת עדיפות לאף אחת מפריסות SceneStrategy
האחרות.
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) ) }
דוגמה: פריסה בסיסית של שני חלונות (סצנה ואסטרטגיה מותאמות אישית)
בדוגמה הזו מוצג איך ליצור פריסה פשוטה עם שני חלונות שמופעלת על סמך שני תנאים:
- רוחב החלון רחב מספיק כדי לתמוך בשתי חלוניות (כלומר, לפחות
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() } 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 } } }
כדי להשתמש ב-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
בתרחיש השימוש list-detail, הארטיפקט androidx.compose.material3.adaptive:adaptive-navigation3
מספק ListDetailSceneStrategy
שיוצר Scene
list-detail. הפריסה הזו Scene
מטפלת אוטומטית בסידורים מורכבים של כמה חלוניות (רשימה, פרטים וחלוניות נוספות) ומתאימה אותם בהתאם לגודל החלון ולמצב המכשיר.
כדי ליצור רכיב Material list-detail 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") } } ) } } } }