הסבר על היסודות והטמעה שלהם

הניווט מתאר את הדרך שבה המשתמשים עוברים באפליקציה. המשתמשים מקיימים אינטראקציה עם רכיבי ממשק המשתמש, בדרך כלל על ידי הקשה או לחיצה עליהם, והאפליקציה מגיבה בהצגת תוכן חדש. אם המשתמש רוצה לחזור לתוכן הקודם, הוא משתמש בתנועת החזרה או מקיש על לחצן החזרה.

בניית מודל של מצב הניווט

דרך נוחה למדל את ההתנהגות הזו היא באמצעות ערימת תוכן. כשהמשתמש עובר קדימה לתוכן חדש, הוא מועבר לראש הערימה. כשמשתמשים לוחצים על הקודם כדי לחזור מהתוכן הזה, הוא מוסר מהמחסנית והתוכן הקודם מוצג. במונחים של ניווט, בדרך כלל מתייחסים למערך הזה כאל מקבץ פעילויות קודמות (back stack), כי הוא מייצג את התוכן שאפשר לחזור אליו.

כפתור פעולה במקלדת וירטואלית (סמל של סימן וי) מוקף בעיגול אדום.
איור 1. תרשים שמראה איך מחסנית החזרה משתנה עם אירועי ניווט של משתמשים.

יצירת מחסנית חזרה

ב-Navigation 3, מקבץ הפעילויות הקודמות (back stack) לא מכיל תוכן בפועל. במקום זאת, הוא מכיל הפניות לתוכן, שנקראות מפתחות. המפתחות יכולים להיות מכל סוג, אבל בדרך כלל הם מחלקות נתונים פשוטות שניתנות לסריאליזציה. השימוש בהפניות במקום בתוכן מניב את היתרונות הבאים:

  • קל לנווט באמצעות הקשה על מקשים במקבץ פעילויות קודמות (back stack).
  • כל עוד המפתחות ניתנים לסריאליזציה, אפשר לשמור את מקבץ הפעילויות הקודמות (back stack) באחסון מתמיד, כך שהיא תמשיך להתקיים שינויים בהגדרות והשבתת תהליך. זה חשוב כי המשתמשים מצפים לצאת מהאפליקציה, לחזור אליה מאוחר יותר ולהמשיך מהמקום שבו הם הפסיקו, עם אותו תוכן שמוצג. מידע נוסף זמין במאמר בנושא שמירת מקבץ פעילויות קודמות (back stack).

מושג מרכזי ב-Navigation 3 API הוא שאתם הבעלים של מקבץ פעילויות קודמות (back stack). הספרייה:

  • הפונקציה מצפה שמקבץ הפעילויות הקודמות (back stack) יהיה List<T> עם גיבוי של מצב תמונת המצב, כאשר T הוא הסוג של מקבץ הפעילויות הקודמות (back stack) keys. אפשר להשתמש בAny או לספק מפתחות משלכם עם הקלדה חזקה יותר. כשרואים את המונחים push או pop, ההטמעה הבסיסית היא הוספה או הסרה של פריטים מסוף הרשימה.
  • הוא עוקב אחרי מקבץ הפעילויות הקודמות (back stack) ומציג את המצב שלה בממשק המשתמש באמצעות NavDisplay.

בדוגמה הבאה מוצג איך ליצור מפתחות ומקבץ פעילויות קודמות (back stack), ולשנות את מקבץ הפעילויות הקודמות (back stack) בתגובה לאירועי ניווט של משתמשים:

// Define keys that will identify content
data object ProductList
data class ProductDetail(val id: String)

@Composable
fun MyApp() {

    // Create a back stack, specifying the key the app should start with
    val backStack = remember { mutableStateListOf<Any>(ProductList) }

    // Supply your back stack to a NavDisplay so it can reflect changes in the UI
    // ...more on this below...

    // Push a key onto the back stack (navigate forward), the navigation library will reflect the change in state
    backStack.add(ProductDetail(id = "ABC"))

    // Pop a key off the back stack (navigate back), the navigation library will reflect the change in state
    backStack.removeLastOrNull()
}

פתרון מפתחות לתוכן

התוכן מודל ב-Navigation 3 באמצעות NavEntry, שהיא מחלקה שמכילה פונקציה קומפוזבילית. הוא מייצג יעד – פריט תוכן יחיד שהמשתמש יכול לנווט אליו ולנווט ממנו.

NavEntry יכול להכיל גם מטא-נתונים – מידע על התוכן. אובייקטים של קונטיינרים, כמו NavDisplay, יכולים לקרוא את המטא-נתונים האלה כדי להחליט איך להציג את התוכן של NavEntry. לדוגמה, אפשר להשתמש במטא-נתונים כדי לשנות את אנימציות ברירת המחדל של NavEntry ספציפי. ‫NavEntry metadata היא מפה של String מפתחות לAny ערכים, שמאפשרת אחסון נתונים מגוון.

כדי להמיר key ל-NavEntry, יוצרים ספק רשומות. זוהי פונקציה שמקבלת key ומחזירה NavEntry עבור אותו key. בדרך כלל הוא מוגדר כפרמטר lambda כשיוצרים NavDisplay.

יש שתי דרכים ליצור ספק רשומות: ליצור פונקציית lambda ישירות או להשתמש ב-DSL‏ entryProvider.

יצירה ישירה של פונקציה של ספק כניסה

בדרך כלל יוצרים פונקציית Entry Provider באמצעות הצהרת when, עם הסתעפות לכל אחד מהמפתחות.

entryProvider = { key ->
    when (key) {
        is ProductList -> NavEntry(key) { Text("Product List") }
        is ProductDetail -> NavEntry(
            key,
            metadata = mapOf("extraDataKey" to "extraDataValue")
        ) { Text("Product ${key.id} ") }

        else -> {
            NavEntry(Unit) { Text(text = "Invalid Key: $it") }
        }
    }
}

שימוש ב-DSL של entryProvider

entryProvider שפת התחום הספציפי יכולה לפשט את פונקציית ה-lambda שלכם, כי לא תצטרכו לבדוק כל אחד מסוגי המפתחות וליצור NavEntry לכל אחד מהם. לשם כך, משתמשים בפונקציית ה-builder‏ entryProvider. הוא כולל גם התנהגות ברירת מחדל (הצגת שגיאה) אם המפתח לא נמצא.

entryProvider = entryProvider {
    entry<ProductList> { Text("Product List") }
    entry<ProductDetail>(
        metadata = mapOf("extraDataKey" to "extraDataValue")
    ) { key -> Text("Product ${key.id} ") }
}

שימו לב לפרטים הבאים בקטע הקוד:

  • entry משמש להגדרת NavEntry עם הסוג הנתון והתוכן שניתן להרכבה
  • entry מקבל פרמטר metadata כדי להגדיר את NavEntry.metadata

הצגת מקבץ פעילויות קודמות (back stack)

מקבץ פעילויות קודמות (back stack) מייצג את מצב הניווט באפליקציה. בכל פעם שמקבץ הפעילויות הקודמות (back stack) משתנה, ממשק המשתמש של האפליקציה צריך לשקף את המצב החדש של מקבץ הפעילויות הקודמות (back stack). ב-Navigation 3, ‏ NavDisplay עוקב אחרי מקבץ הפעילויות הקודמות (back stack) ומעדכן את ממשק המשתמש בהתאם. יוצרים אותו עם הפרמטרים הבאים:

  • מקבץ פעילויות קודמות (back stack) – הערך הזה צריך להיות מסוג SnapshotStateList<T>, כאשר T הוא הסוג של המפתחות במקבץ פעילויות קודמות (back stack). הוא Listobservable כדי שהוא יפעיל recomposition של NavDisplay כשהוא משתנה.
  • entryProvider כדי להמיר את המפתחות במקבץ פעילויות קודמות (back stack) לאובייקטים.NavEntry
  • אפשר גם לספק פונקציית למדה לפרמטר onBack. הפונקציה הזו מופעלת כשהמשתמש מפעיל אירוע חזרה.

בדוגמה הבאה אפשר לראות איך יוצרים NavDisplay.

data object Home
data class Product(val id: String)

@Composable
fun NavExample() {

    val backStack = remember { mutableStateListOf<Any>(Home) }

    NavDisplay(
        backStack = backStack,
        onBack = { backStack.removeLastOrNull() },
        entryProvider = { key ->
            when (key) {
                is Home -> NavEntry(key) {
                    ContentGreen("Welcome to Nav3") {
                        Button(onClick = {
                            backStack.add(Product("123"))
                        }) {
                            Text("Click to navigate")
                        }
                    }
                }

                is Product -> NavEntry(key) {
                    ContentBlue("Product ${key.id} ")
                }

                else -> NavEntry(Unit) { Text("Unknown route") }
            }
        }
    )
}

כברירת מחדל, NavDisplay מציג את NavEntry העליון במקבץ פעילויות קודמות (back stack) בפריסת חלונית יחידה. ההקלטה הבאה מציגה את האפליקציה הזו בפעולה:

התנהגות ברירת המחדל של `NavDisplay` עם שני יעדים.
איור 2. NavDisplay התנהגות ברירת המחדל עם שני יעדים.

מחזור החיים של היעד

NavDisplay משתמש בLifecycleOwnerים מותאמים אישית כדי להגביל את מצב מחזור החיים של NavEntry על סמך אילוצים ברמת הסצנה ואילוצים ברמת הכניסה.

מידע נוסף על מחזורי חיים ב-Compose זמין במאמר מחזור חיים ב-Jetpack Compose.

מגבלות על מחזור החיים ברמת הסצנה

NavDisplay מנהל את מחזור החיים של Scene הפעיל. הגבלות ברמת הסצנה נקבעות באופן הבא:

בסצנות שאינן שכבת-על:

  • RESUMED: מותר רק כשהמעבר בין הסצנות הסתיים ואין סצנות פעילות של שכבות-על שמוצגות מעל הסצנה.
  • STARTED: הערך מוגבל ל-STARTED בזמן מעבר בין סצנות, למשל כשמנווטים קדימה או אחורה, או כשהוא מכוסה בשכבת-על.

לסצנות שכבת-על, כמו תיבות דו-שיח או גיליונות תחתונים:

  • RESUMED: מותר רק עבור סצנת שכבת העל העליונה שפעילה כרגע.
  • STARTED: מוגבל ל-STARTED לכל סצנות שכבת-על בסיסיות שנכללות בשכבת-על חדשה יותר.

מצב מחזור חיים בסיסי

הספרייה מנהלת את מצב מחזור החיים המקסימלי של כל NavEntry בנפרד, בהתאם לנוכחות שלו במקבץ הפעילויות הקודמות (back stack):

  • RESUMED: אם הרשומה קיימת במצבור הפעולות האחרונות הנוכחי, מחזור החיים שלה יכול להגיע עד RESUMED (בכפוף למגבלה ברמת הסצנה).
  • CREATED: אם הרשומה כבר לא נמצאת במקבץ הפעילויות הקודמות (back stack), למשל אם היא הוצאה ממנה אבל עדיין מוצגת במסך בזמן שהיא יוצאת מהאנימציה, הספרייה מגבילה את מחזור החיים שלה ל-CREATED. המגבלה הזו מבטיחה שרשומות ברקע או רשומות שיוצאות יפסיקו לבצע עבודה פעילה, כמו איסוף של זרימות או הפעלה של קורוטינות שקשורות למצבי RESUMED או STARTED, בזמן שהן מסיימות את מעברי היציאה שלהן.

איך הן משולבות

לדוגמה, מצב מחזור החיים הסופי של NavEntry נקבע באופן הבא:

תרחיש מכסה ברמת הסצנה מכסה למתחילים מכסה אפקטיבית
כניסה פעילה, מסך קבוע (ללא מעברים או שכבות-על) RESUMED RESUMED RESUMED
כניסה פעילה, במהלך מעבר (ניווט אל או מתוך) STARTED RESUMED STARTED
רשומה פעילה, מכוסה בשכבת-על (לדוגמה, תיבת דו-שיח פתוחה) STARTED RESUMED STARTED
רשומה שמוצגת בחלון קופץ, עם אנימציה של יציאה STARTED או RESUMED CREATED CREATED

סיכום של כל המידע

בתרשים הבא מוצג זרימת הנתונים בין האובייקטים השונים בגרסה 3 של Navigation:

המחשה של זרימת הנתונים בין האובייקטים השונים בגרסה 3 של הניווט.
איור 3. דיאגרמה שמראה איך הנתונים זורמים דרך אובייקטים שונים ב-Navigation 3.
  1. אירועי ניווט יוזמים שינויים. מפתחות נוספים או מוסרים ממקבץ הפעילויות הקודמות (back stack) בתגובה לאינטראקציות של המשתמשים.

  2. שינוי במצב של מקבץ הפעילויות הקודמות (back stack) מפעיל אחזור תוכן. הקומפוזבילי NavDisplay (a composable that renders a back stack) עוקב אחרי מקבץ הפעילויות הקודמות. במצב ברירת המחדל, הוא מציג את הרשומה העליונה במקבץ פעילויות קודמות (back stack) בפריסת חלונית אחת. כשמקש העליון במחסנית האחורית משתנה, NavDisplay משתמש במקש הזה כדי לבקש את התוכן המתאים מספק הרשומות.

  3. ספק התוכן מספק תוכן. ספק הכניסה הוא פונקציה שמפענחת מפתח ל-NavEntry. כשספק הכניסה מקבל מפתח מ-NavDisplay, הוא מספק את NavEntry המשויך, שמכיל גם את המפתח וגם את התוכן.

  4. התוכן מוצג. ‫NavDisplay מקבל את NavEntry ומציג את התוכן.