פיתוח גרף באופן פרוגרמטי באמצעות Kotlin DSL

רכיב הניווט מספק שפה ספציפית לדומיין מבוססת-Kotlin, או DSL, שמסתמכת על type-safe builders. ה-API הזה מאפשר להרכיב באופן הצהרתי את התרשים בקוד Kotlin שלך, במקום בתוך משאב XML. היא יכולה להיות שימושית אם רוצים ליצור לניווט דינמי באפליקציה. לדוגמה, האפליקציה יכולה להוריד וגם לשמור תצורת ניווט משירות אינטרנט חיצוני ואז להשתמש את התצורה הזו כדי ליצור באופן דינמי תרשים ניווט onCreate().

יחסי תלות

כדי להשתמש ב-Kotlin DSL, צריך להוסיף את התלות הבאה ל- קובץ build.gradle:

מגניב

dependencies {
    def nav_version = "2.7.7"

    api "androidx.navigation:navigation-fragment-ktx:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.7.7"

    api("androidx.navigation:navigation-fragment-ktx:$nav_version")
}

יצירת תרשים

נתחיל בדוגמה בסיסית שמבוססת על אפליקציית 'חמנית'. בשביל זה לדוגמה, יש לנו שני יעדים: home ו-plant_detail. home היעד מוצג כשהמשתמש מפעיל את האפליקציה בפעם הראשונה. היעד הזה מציג רשימה של צמחים מהגינה של המשתמש. כשהמשתמש בוחר באחת מהאפשרויות הבאות: הצמחים, האפליקציה מנווטת ליעד plant_detail.

איור 1 מציג את היעדים האלה יחד עם הארגומנטים הנדרשים יעד אחד (plant_detail) ופעולה, to_plant_detail, שהאפליקציה משתמשת בהם כדי לנווט מhome אל plant_detail.

אפליקציית Sunflower כוללת שני יעדים
            שמחבר ביניהם.
איור 1. לאפליקציית Sunflower יש שני יעדים: home וגם plant_detail, יחד עם פעולה שמחבר ביניהם.

אירוח תרשים Nav של Kotlin DSL

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

<!-- activity_garden.xml -->
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />

</FrameLayout>

שימו לב שהמאפיין app:navGraph לא מוגדר בדוגמה הזו. התרשים לא מוגדר משאב תיקייה אחת (res/navigation), לכן צריך להגדיר אותה כחלק מonCreate() במהלך הפעילות.

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

השלב הבא הוא להגדיר כמה קבועים שבהם תשתמשו את התרשים.

יוצרים קבועים לתרשים

תרשימי ניווט מבוססי XML מנותחים כחלק מתהליך ה-build של Android. נוצר קבוע מספרי לכל מאפיין id שמוגדר בתרשים. זמן ה-build האלה יצר המזהים לא זמינים כשיוצרים את תרשים הניווט בזמן הריצה, ב-DSL של ניווט נעשה שימוש במחרוזות נתיב במקום במזהים. כל נתיב מיוצג על ידי מחרוזת ייחודית ומומלץ להגדיר אותם כקבועים כדי לצמצם את הסיכון לבאגים שקשורים להקלדה.

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

object nav_routes {
    const val home = "home"
    const val plant_detail = "plant_detail"
}

object nav_arguments {
    const val plant_id = "plant_id"
    const val plant_name = "plant_name"
}

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

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = nav_routes.home
) {
    fragment<HomeFragment>(nav_routes.home) {
        label = resources.getString(R.string.home_title)
    }

    fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
        label = resources.getString(R.string.plant_detail_title)
        argument(nav_arguments.plant_id) {
            type = NavType.StringType
        }
    }
}

בדוגמה זו, ה-lambda בסוף מגדיר שני יעדי מקטעים באמצעות התג fragment() הפונקציה Builder DSL. הפונקציה הזו מחייבת מחרוזת נתיב ליעד שמתקבל מהקבועים. הפונקציה גם מקבלת lambda להגדרות נוספות, כמו תווית היעד והפונקציות של ה-builder המוטמע לארגומנטים ולקישורי עומק.

הסיווג Fragment מנהל את ממשק המשתמש של כל יעד, מועבר כסוג פרמטר בתוך סוגריים זוויתיים (<>). הפעולה הזו משפיעה כמו הגדרה של android:name ביעדי מקטעים שמוגדרים באמצעות XML.

לבסוף, אפשר לנווט מhome אל plant_detail באמצעות NavController.navigate() שיחות:

private fun navigateToPlant(plantId: String) {
   findNavController().navigate("${nav_routes.plant_detail}/$plantId")
}

בפונקציה PlantDetailFragment אפשר לקבל את ערך הארגומנט כמו שמוצג בדוגמה הבאה:

val plantId: String? = arguments?.getString(nav_arguments.plant_id)

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

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

יעדים

Kotlin DSL מספק תמיכה מובנית לשלושה סוגי יעדים: היעדים Fragment, Activity ו-NavGraph, ולכל אחד מהם יש יעד משלו פונקציית התוספים המוטבעת זמינה לבנייה ולהגדרה של היעד.

יעדי מקטעים

fragment() אפשר לתת פרמטרים של פונקציית DSL לסיווג המקטעים המוטמעים והיא לוקחת מחרוזת נתיב ייחודית שצריך להקצות ליעד הזה, ואחריה lambda, אפשר לספק הגדרות אישיות נוספות כמו שמתואר ניווט באמצעות תרשים ה-DSL של Kotlin .

fragment<FragmentDestination>(nav_routes.route_name) {
   label = getString(R.string.fragment_title)
   // arguments, deepLinks
}

יעד הפעילות

activity() פונקציית DSL משתמשת במחרוזת נתיב ייחודית כדי להקצות ליעד הזה, אבל לא מוגדרים כפרמטרים של סיווג פעילות מיושמת. במקום זאת, צריך להגדיר אופציונלי activityClass ב-lambda בסוף. הגמישות הזו מאפשרת להגדיר יעד פעילות לפעילות שתופעל באמצעות אובייקט Intent מרומז, שבו אין משמעות לסיווג פעילות בוטה. בדומה ליעדים עם מקטעים, אפשר גם להגדיר תווית, ארגומנטים וקישורי עומק.

activity(nav_routes.route_name) {
   label = getString(R.string.activity_title)
   // arguments, deepLinks...

   activityClass = ActivityDestination::class
}

navigation() אפשר להשתמש בפונקציית DSL כדי תרשים ניווט מוטמע. הפונקציה הזו לוקחת שלושה ארגומנטים: נתיב אל להקצות לתרשים, את נתיב היעד ההתחלתי של הגרף lambda כדי להגדיר את התרשים עוד יותר. האלמנטים החוקיים כוללים יעדים אחרים, ארגומנטים, קישורי עומק תווית תיאורית ליעד. התווית הזו יכולה להיות שימושית לקישור תרשים הניווט לממשק המשתמש רכיבים באמצעות NavigationUI

navigation("route_to_this_graph", nav_routes.home) {
   // label, other destinations, deep links
}

יעדים מותאמים אישית

אם משתמשים סוג יעד חדש שלא כולל לתמיכה ישירה ב-Kotlin DSL, אפשר להוסיף את היעדים האלה ל-Kotlin DSL באמצעות addDestination():

// The NavigatorProvider is retrieved from the NavController
val customDestination = navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}
addDestination(customDestination)

לחלופין, אפשר להשתמש גם באופרטור unary Plus כדי להוסיף יעד מובנה ישירות לתרשים:

// The NavigatorProvider is retrieved from the NavController
+navigatorProvider[CustomNavigator::class].createDestination().apply {
    route = Graph.CustomDestination.route
}

הצגת ארגומנטים של יעד

כל יעד יכול להגדיר ארגומנטים שהם אופציונליים או שהם נדרשים. פעולות אפשר להגדיר באמצעות argument() בפונקציה NavDestinationBuilder, שהיא המחלקה הבסיסית הסוגים של הכלי ליצירת יעדים. הפונקציה הזו לוקחת את שם הארגומנט כמחרוזת ולמבדה שמשמשת לבנייה ולהגדרה NavArgument

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

fragment<PlantDetailFragment>("${nav_routes.plant_detail}/{${nav_arguments.plant_id}}") {
    label = getString(R.string.plant_details_title)
    argument(nav_arguments.plant_id) {
        type = NavType.StringType
        defaultValue = getString(R.string.default_plant_id)
        nullable = true  // default false
    }
}

אם צוין defaultValue, אפשר להסיק את הסוג. אם גם defaultValue ומציינים type, הסוגים חייבים להתאים. לצפייה מאמרי עזרה של NavType הרשימה המלאה של סוגי הארגומנטים הזמינים.

מתן סוגים מותאמים אישית

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

לדוגמה, סיווג נתונים שמייצג פרמטרים של חיפוש שמועברים מסך החיפוש יכול להטמיע גם את Serializable (כדי לספק את תמיכה בקידוד/פענוח) וב-Parcelize (לתמיכה בשמירה ובשחזור מ-Bundle):

@Serializable
@Parcelize
data class SearchParameters(
  val searchQuery: String,
  val filters: List<String>
)

אפשר לכתוב NavType בהתאמה אישית כך:

val SearchParametersType = object : NavType<SearchParameters>(
  isNullableAllowed = false
) {
  override fun put(bundle: Bundle, key: String, value: SearchParameters) {
    bundle.putParcelable(key, value)
  }
  override fun get(bundle: Bundle, key: String): SearchParameters {
    return bundle.getParcelable(key) as SearchParameters
  }

  override fun serializeAsValue(value: SearchParameters): String {
    // Serialized values must always be Uri encoded
    return Uri.encode(Json.encodeToString(value))
  }

  override fun parseValue(value: String): SearchParameters {
    // Navigation takes care of decoding the string
    // before passing it to parseValue()
    return Json.decodeFromString<SearchParameters>(value)
  }
}

לאחר מכן אפשר להשתמש בו ב-Kotlin DSL כמו כל סוג אחר:

fragment<SearchFragment>(nav_routes.plant_search) {
    label = getString(R.string.plant_search_title)
    argument(nav_arguments.search_parameters) {
        type = SearchParametersType
        defaultValue = SearchParameters("cactus", emptyList())
    }
}

השדה NavType כולל גם את הכתיבה וגם את הקריאה בכל שדה, כלומר, חייבים להשתמש ב-NavType גם כשמנווטים אל כדי להבטיח שהפורמטים תואמים:

val params = SearchParameters("rose", listOf("available"))
val searchArgument = SearchParametersType.serializeAsValue(params)
navController.navigate("${nav_routes.plant_search}/$searchArgument")

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

val params: SearchParameters? = arguments?.getParcelable(nav_arguments.search_parameters)

קישורי עומק

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

כשיוצרים קישור עומק מרומז אבל אין לכם משאב ניווט בפורמט XML שניתן לנתח רכיבי <deepLink>. לכן, לא ניתן להסתמך על הצבת <nav-graph> בקובץ AndroidManifest.xml, וצריך להוסיף במקום זאת מסנני כוונת רכישה לפעילות שלכם באופן ידני. מסנן Intent שאתם מציינים צריך להתאים לתבנית כתובת האתר הבסיסית, לפעולה ול mimetype של קישורי העומק באפליקציה.

אפשר לספק ערך deeplink ספציפי יותר לכל קישור עומק בנפרד יעד באמצעות deepLink() DSL. הפונקציה הזו מקבלת NavDeepLink שמכיל String שמייצג את דפוס ה-URI, פרמטר String שמייצג את פעולות Intent, String מייצג את mimeType .

לדוגמה:

deepLink {
    uriPattern = "http://www.example.com/plants/"
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
}

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

תרחיש מורכב יותר של קישורי עומק מרומזים שמגדיר גם נתיב בטבלה הבאה מוצגים פרמטרים מבוססי-שאילתות:

val baseUri = "http://www.example.com/plants"

fragment<PlantDetailFragment>(nav_routes.plant_detail) {
   label = getString(R.string.plant_details_title)
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}"
   })
   deepLink(navDeepLink {
    uriPattern = "${baseUri}/{id}?name={plant_name}"
   })
}

אפשר להשתמש אינטרפולציה של מחרוזות כדי לפשט את ההגדרה.

מגבלות

הפלאגין Safe Args הוא לא תואם ל-Kotlin DSL, כי הפלאגין מחפש קובצי משאבים של XML כדי ליצור Directions ו-Arguments מחלקות.

מידע נוסף

כדאי לקרוא את המאמר בטיחות סוג הניווט. כדי ללמוד איך לספק בטיחות סוג ל-Kotlin DSL קוד לכתיבה של ניווט.