Navigation 3 מציג מערכת עוצמתית וגמישה לניהול זרימת ממשק המשתמש של האפליקציה באמצעות Scenes. סצנות מאפשרות ליצור פריסות מותאמות אישית מאוד, להתאים את הפריסה לגדלים שונים של מסכים ולנהל בצורה חלקה חוויות מורכבות עם חלונות מרובים.
הסבר על סצנות
ב-Navigation 3, 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>>: המאפיין הזה מגדיר אתNavEntrys שיוצגו אם תתבצע פעולת 'חזרה' מה-Sceneהנוכחי. הוא חיוני לחישוב מצב החזרה הקודם החזוי הנכון, ומאפשר ל-NavDisplayלצפות את המצב הקודם הנכון ולעבור אליו. המצב הקודם יכול להיות Scene עם מחלקה, מפתח או שניהם שונים. -
content: @Composable () -> Unit: זו פונקציה שאפשר להרכיב ממנה פונקציות אחרות, שבה מגדירים איך הרכיבSceneמעבד את הרכיבentriesואת כל רכיבי ממשק המשתמש שמסביב שספציפיים לרכיבScene. -
metadata: Map<String, Any>: מספק מידע ספציפי לסצנה לרכיבים אחרים בספרייה, כמוNavDisplay. כברירת מחדל, הפונקציה מחזירה את הערךmetadataשלNavEntryהאחרון ב-entries.
הטמעה של equals ו-hashCode בסצנות מותאמות אישית
הטמעות מותאמות אישית של Scene צריכות להטמיע את equals ואת hashCode בצורה נכונה כדי לתמוך בפעולות הבאות:
- מעברים בין מצבי סצנה: בכל פעם שרשימת אסטרטגיות הסצנה מחשבת סצנה,
NavDisplayבודק אם הסצנה החדשה זהה לסצנה הקודמת. אם מזוהה שינוי, המערכת מעדכנת אתSeekableTransitionused to transition between scenes, מה שמשפיע על מצב מחזור החיים של הסצנות. - ניהול שכבות-על: עבור
OverlayScenes (כמו תיבות דו-שיח),NavDisplayמשתמש באובייקט הסצנה עצמו כ-keyכדי לעקוב אחרי מחזורי החיים של שכבות-על ואנימציות יציאה.
מוודאים שכל המאפיינים שמגדירים את התוכן של הסצנה, כמו key,
entries ו-previousEntries, כלולים בהטמעות של equals ו-hashCode. בדרך כלל, כדאי להימנע מהכללת קריאות חוזרות (כמו onBack) בהטמעה של השיטות האלה, כי הן יכולות לשנות מופעים בלי לשנות את המצב הלוגי של הסצנה.
הסבר על אסטרטגיות של סצנות
SceneStrategy הוא המנגנון שקובע איך צריך לסדר רשימה נתונה של NavEntrys ממחסנית החזרה, ואיך לבצע מעבר ל-Scene. בעצם, כשמוצגים ל-SceneStrategy הערכים הנוכחיים במחסנית החזרה, הוא שואל את עצמו שתי שאלות מרכזיות:
- האם אפשר ליצור
Sceneמהערכים האלה? אםSceneStrategyקובע שהוא יכול לטפל בNavEntryהנתון וליצורSceneמשמעותי (למשל, תיבת דו-שיח או פריסה מרובת חלוניות), הוא ממשיך. אחרת, היא מחזירה אתnull, כדי לאפשר לשיטות אחרות ליצורScene. - אם כן, איך צריך לסדר את הרשומות האלה ב-
Scene?אחרי ש-SceneStrategyמתחייב לטפל ברשומות, הוא לוקח על עצמו את האחריות ליצירתSceneולהגדרת האופן שבוNavEntrys יופיעו ב-Scene.
הליבה של SceneStrategy היא ה-method calculateScene:
@Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>?
השיטה הזו היא פונקציית הרחבה ב-SceneStrategyScope שמקבלת את List<NavEntry<T>> הנוכחי ממקבץ הפעילויות הקודמות (back stack). הפונקציה צריכה להחזיר Scene<T> אם היא יכולה ליצור רשימה מהערכים שסופקו, או null אם היא לא יכולה.
האובייקט SceneStrategyScope אחראי לשמירה של כל הארגומנטים האופציונליים שאולי נדרשים ל-SceneStrategy, כמו onBack callback.
איך סצנות ושיטות בידינג לסצנות פועלות יחד
NavDisplay הוא רכיב מרכזי שניתן להרכבה, שעוקב אחרי ערימת הפעולות הקודמות ומשתמש באחד או יותר מ-SceneStrategy כדי לקבוע ולעבד את Scene המתאים.
הפרמטר NavDisplay's sceneStrategies מצפה לרשימה של SceneStrategy
מופעים שאחראים לחישוב של Scene שיוצג. אם המערכת לא מצליחה לחשב Scene באמצעות השיטות שצוינו, היא חוזרת אוטומטית לשימוש בSinglePaneSceneStrategy כברירת מחדל.NavDisplay
פירוט האינטראקציה:
- כשמוסיפים או מסירים מקשים ממערך החזרה (לדוגמה, באמצעות
backStack.add()אוbackStack.removeLastOrNull()),NavDisplayעוקב אחרי השינויים האלה. - הפונקציה
NavDisplayמעבירה את הרשימה הנוכחית שלNavEntrys (שנגזרת ממפתחות ה-backstack) אלsceneStrategiesשהוגדר, לפי הסדר, ומפעילה אתcalculateSceneעל כל אחד מהם עד שמוחזרScene. - כש-
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> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? = SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1) ) }
דוגמה: פריסת רשימה עם פרטים (סצנה ואסטרטגיה מותאמות אישית)
בדוגמה הזו אפשר לראות איך ליצור פריסת רשימה עם פרטים שמופעלת על סמך שני תנאים:
- רוחב החלון רחב מספיק כדי לתמוך בשתי חלוניות (כלומר, לפחות
WIDTH_DP_MEDIUM_LOWER_BOUND). - מחסנית החזרה מכילה רשומות שהצהירו על תמיכה בהצגה בפריסת רשימה-פרטים באמצעות מטא-נתונים ספציפיים.
קטע הקוד הבא הוא קוד המקור של ListDetailScene.kt והוא מכיל את ListDetailScene וגם את ListDetailSceneStrategy:
// --- ListDetailScene --- /** * A [Scene] that displays a list and a detail [NavEntry] side-by-side in a 40/60 split. * */ data class ListDetailScene<T : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, val listEntry: NavEntry<T>, val detailEntry: NavEntry<T>, ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(listEntry, detailEntry) override val content: @Composable (() -> Unit) = { Row(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.weight(0.4f)) { listEntry.Content() } Column(modifier = Modifier.weight(0.6f)) { detailEntry.Content() } } } } @Composable fun <T : Any> rememberListDetailSceneStrategy(): ListDetailSceneStrategy<T> { val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass return remember(windowSizeClass) { ListDetailSceneStrategy(windowSizeClass) } } // --- ListDetailSceneStrategy --- /** * A [SceneStrategy] that returns a [ListDetailScene] if the window is wide enough, the last item * is the backstack is a detail, and before it, at any point in the backstack is a list. */ class ListDetailSceneStrategy<T : Any>(val windowSizeClass: WindowSizeClass) : SceneStrategy<T> { override fun SceneStrategyScope<T>.calculateScene(entries: List<NavEntry<T>>): Scene<T>? { if (!windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)) { return null } val detailEntry = entries.lastOrNull()?.takeIf { it.metadata.contains(DetailKey) } ?: return null val listEntry = entries.findLast { it.metadata.contains(ListKey) } ?: return null // We use the list's contentKey to uniquely identify the scene. // This allows the detail panes to be displayed instantly through recomposition, rather than // having NavDisplay animate the whole scene out when the selected detail item changes. val sceneKey = listEntry.contentKey return ListDetailScene( key = sceneKey, previousEntries = entries.dropLast(1), listEntry = listEntry, detailEntry = detailEntry ) } object ListKey : NavMetadataKey<Boolean> object DetailKey : NavMetadataKey<Boolean> companion object { /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun listPane() = metadata { put(ListKey, true) } /** * Helper function to add metadata to a [NavEntry] indicating it can be displayed * as a list in the [ListDetailScene]. */ fun detailPane() = metadata { put(DetailKey, true) } } }
כדי להשתמש ב-ListDetailSceneStrategy הזה ב-NavDisplay, צריך לשנות את הקריאות ל-entryProvider כך שיכללו מטא-נתונים של ListDetailScene.listPane() עבור הרשומה שרוצים להציג בפריסת רשימה, ואת ListDetailScene.detailPane() עבור הרשומה שרוצים להציג בפריסת פרטים. לאחר מכן, מציינים ListDetailSceneStrategy() כערך של sceneStrategy,
ומסתמכים על ברירת המחדל של חזרה למצב הקודם בתרחישים של חלון יחיד:
// Define your navigation keys @Serializable data object ConversationList : NavKey @Serializable data class ConversationDetail(val id: String) : NavKey @Composable fun MyAppContent() { val backStack = rememberNavBackStack(ConversationList) val listDetailStrategy = rememberListDetailSceneStrategy<NavKey>() NavDisplay( backStack = backStack, onBack = { backStack.removeLastOrNull() }, sceneStrategies = listOf(listDetailStrategy), entryProvider = entryProvider { entry<ConversationList>( metadata = ListDetailSceneStrategy.listPane() ) { Column(modifier = Modifier.fillMaxSize()) { Text(text = "I'm a Conversation List") Button(onClick = { backStack.addDetail(ConversationDetail("123")) }) { Text(text = "Open detail") } } } entry<ConversationDetail>( metadata = ListDetailSceneStrategy.detailPane() ) { Text(text = "I'm a Conversation Detail") } } ) } private fun NavBackStack<NavKey>.addDetail(detailRoute: ConversationDetail) { // Remove any existing detail routes, then add the new detail route removeIf { it is ConversationDetail } add(detailRoute) }
אם אתם לא רוצים ליצור סצנת פרטי רשימה משלכם, אתם יכולים להשתמש בסצנת פרטי רשימה של Material, שכוללת פרטים הגיוניים ותמיכה ב-placeholder, כמו שמוצג בקטע הבא.
הצגת תוכן של רשימה ופרטים בסצנה מותאמת של Material
בתרחיש השימוש list-detail, פריט המידע שנוצר בתהליך פיתוח (Artifact) androidx.compose.material3.adaptive:adaptive-navigation3 מספק ListDetailSceneStrategy שיוצר Scene list-detail. הפריסה הזו Scene
מטפלת אוטומטית בסידורים מורכבים של כמה חלונות (רשימה, פרטים וחלונות נוספים) ומתאימה אותם בהתאם לגודל החלון ולמצב המכשיר.
כדי ליצור רשימה עם פרטים ב-Material 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<NavKey>() NavDisplay( backStack = backStack, modifier = Modifier.padding(paddingValues), onBack = { backStack.removeLastOrNull() }, sceneStrategies = listOf(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") } } ) } } } }