قطعه‌ها و Kotlin DSL

مؤلفه Navigation یک زبان خاص دامنه مبتنی بر Kotlin یا DSL ارائه می‌دهد که به سازندگان ایمن ازنظر نوع Kotlin متکی است. این میانای برنامه‌سازی کاربردی به شما امکان می‌دهد به‌جای اینکه نمودارتان را در منبع XML ترکیب کنید، آن را به‌صورت اعلانی در کد Kotlin ترکیب کنید. این کار می‌تواند زمانی مفید باشد که بخواهید ناوبری برنامه‌تان را به‌صورت پویا بسازید. برای مثال، برنامه‌تان می‌تواند پیکربندی پیمایش را از سرویس وب خارجی بارگیری و ذخیره موقت کند و سپس از آن پیکربندی برای ساختن پویا نمودار پیمایش در تابع onCreate() فعالیتتان استفاده کند.

وابستگی‌ها

برای استفاده از Kotlin DSL با Fragment، وابستگی زیر را به فایل build.gradle برنامه‌تان اضافه کنید:

گرووی

dependencies {
    def nav_version = "2.10.2"

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

کاتلین

dependencies {
    val nav_version = "2.10.2"

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

درحال ساختن نمودار

در اینجا نمونه‌ای ساده براساس برنامه Sunflower آورده شده است. برای این نمونه، دو مقصد داریم: home و plant_detail. مقصد home وقتی کاربر برای اولین‌بار برنامه را راه‌اندازی می‌کند وجود دارد. این مقصد فهرستی از گیاهان باغچه کاربر را نمایش می‌دهد. وقتی کاربر یکی از گیاهان را انتخاب می‌کند، برنامه به مقصد plant_detail پیمایش می‌کند.

شکل ۱ این مقصدها را به‌همراه آرگومان‌های موردنیاز مقصد plant_detail و کنشی (to_plant_detail) نشان می‌دهد که برنامه برای پیمایش از home به plant_detail استفاده می‌کند.

برنامه Sunflower دو مقصد به‌همراه کنشی دارد که
            آن‌ها را به‌هم متصل می‌کند.
شکل ۱. برنامه Sunflower دو مقصد دارد، home و plant_detail، به‌همراه کنشی که آن‌ها را متصل می‌کند.

میزبانی از نمودار پیمایش 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، یک کنش شناسه مقصد را با یک یا چند شناسه مرتبط می‌کند. بااین‌حال، هنگام استفاده از Navigation DSL، یک مسیر می‌تواند حاوی آرگومان‌هایی به‌عنوان بخشی از مسیر باشد. این یعنی هنگام استفاده از DSL، مفهومی از کنش‌ها وجود ندارد.

مرحله بعدی تعریف مسیرهایی است که هنگام تعریف نمودار خود استفاده خواهید کرد.

ایجاد مسیر برای گراف

نمودارهای ناوبری مبتنی بر XML به‌عنوان بخشی از فرایند ساخت Android تجزیه می‌شوند. یک ثابت عددی برای هر مشخصه id تعریف‌شده در نمودار ایجاد می‌شود. این شناسه‌های ایستا که در زمان ساخت تولید می‌شوند، هنگام ساختن نمودار ناوبری در زمان اجرا دردسترس نیستند، بنابراین Navigation DSL به‌جای شناسه‌ها از انواع سریال‌سازی‌پذیر استفاده می‌کند. هر مسیر با نوع منحصربه‌فردی نشان داده می‌شود.

هنگام سروکار داشتن با آرگومان‌ها، این موارد در نوع مسیر تعبیه شده‌اند. این کار به شما امکان می‌دهد برای آرگومان‌های پیمایش خود ایمنی تایپی داشته باشید.

@Serializable data object Home
@Serializable data class Plant(val id: String)

پس‌از تعریف مسیرها، می‌توانید نمودار پیمایش را بسازید.

val navController = findNavController(R.id.nav_host_fragment)
navController.graph = navController.createGraph(
    startDestination = Home
) {
    fragment<HomeFragment, Home> {
        label = resources.getString(R.string.home_title)
    }
    fragment<PlantDetailFragment, PlantDetail> {
        label = resources.getString(R.string.plant_detail_title)
    }
}

در این مثال، دو مقصد قطعه بااستفاده از تابع سازنده DSL fragment() تعریف شده‌اند. این تابع به دو آرگومان نوع نیاز دارد.

ابتدا، کلاس Fragment که واسط کاربر را برای این مقصد ارائه می‌دهد. تنظیم این مورد همان تأثیری را دارد که تنظیم مشخصه android:name در مقصد قطعه که بااستفاده از XML تعریف شده است دارد.

دوم، مسیر. این باید نوعی قابل‌سریال‌سازی باشد که از Any گسترش می‌یابد. این کلاس باید شامل همه متغیرهای مستقل ناوبری که توسط این مقصد استفاده خواهد شد و انواع آن‌ها باشد.

این تابع همچنین یک لامبدا اختیاری را برای پیکربندی اضافی، مانند برچسب مقصد، و همچنین توابع سازنده جاسازی‌شده برای آرگومان‌های سفارشی و پیوندهای عمیق می‌پذیرد.

درنهایت، می‌توانید بااستفاده از تماس‌های NavController.navigate() از home به plant_detail پیمایش کنید:

private fun navigateToPlant(plantId: String) {
   findNavController().navigate(route = PlantDetail(id = plantId))
}

در PlantDetailFragment، می‌توانید با دریافت NavBackStackEntry فعلی و فراخوانی toRoute روی آن، آرگومان‌های ناوبری را برای دریافت نمونه مسیر به‌دست آورید.

val plantDetailRoute = findNavController().getBackStackEntry<PlantDetail>().toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

اگر PlantDetailFragment از ViewModel استفاده می‌کند، نمونه مسیر را بااستفاده از SavedStateHandle.toRoute دریافت کنید.

val plantDetailRoute = savedStateHandle.toRoute<PlantDetail>()
val plantId = plantDetailRoute.id

بقیه این راهنما عناصر رایج گراف پیمایش، مقصدها، و نحوه استفاده از آن‌ها را هنگام ساختن گراف شرح می‌دهد.

مقصدها

‫Kotlin DSL پشتیبانی داخلی برای سه نوع مقصد ارائه می‌دهد: مقصدهای Fragment،‏ Activity، و NavGraph که هرکدام تابع افزونه درون‌خطی خود را برای ساختن و پیکربندی مقصد دارند.

مقصدهای قطعه‌ای

تابع DSL fragment() می‌تواند با کلاس قطعه برای واسط کاربر و نوع مسیر استفاده‌شده برای شناسایی منحصربه‌فرد این مقصد پارامتربندی شود، و به‌دنبال آن یک لامبدا که در آن می‌توانید پیکربندی اضافی را همان‌طور که در بخش پیمایش با نمودار Kotlin DSL شرح داده شده است ارائه دهید.

fragment<MyFragment, MyRoute> {
   label = getString(R.string.fragment_title)
   // custom argument types, deepLinks
}

مقصد فعالیت

تابع activity() DSL یک پارامتر نوع برای مسیر می‌گیرد اما برای هیچ کلاس فعالیت اجرایی پارامتربندی نشده است. درعوض، activityClass اختیاری را در لامبدا انتهایی تنظیم می‌کنید. این انعطاف‌پذیری به شما امکان می‌دهد مقصد فعالیتی را برای فعالیتی تعریف کنید که باید بااستفاده از هدف ضمنی راه‌اندازی شود، جایی که کلاس فعالیت صریح معنایی ندارد. همانند مقصدهای قطعه، می‌توانید برچسب، آرگومان‌های سفارشی، و پیوندهای عمیق را نیز پیکربندی کنید.

activity<MyRoute> {
   label = getString(R.string.activity_title)
   // custom argument types, deepLinks...

   activityClass = MyActivity::class
}

از تابع DSL navigation() می‌توان برای ساختن نمودار پیمایش تودرتو استفاده کرد. این تابع یک پارامتر نوع برای مسیر می‌گیرد تا به این نمودار اختصاص دهد. این تابع همچنین دو آرگومان می‌گیرد: مسیر مقصد شروع نمودار، و یک لامبدا برای پیکربندی بیشتر نمودار. عناصر معتبر شامل مقصدهای دیگر، انواع آرگومان سفارشی، پیوندهای عمیق، و برچسب توصیفی برای مقصد است. این برچسب می‌تواند برای اتصال گراف ناوبری به مؤلفه‌های واسط کاربر بااستفاده از NavigationUI مفید باشد.

@Serializable data object HomeGraph
@Serializable data object Home

navigation<HomeGraph>(startDestination = Home) {
   // label, other destinations, deep links
}

پشتیبانی از مقصدهای سفارشی

اگر از نوع مقصد جدیدی استفاده می‌کنید که مستقیماً از Kotlin DSL پشتیبانی نمی‌کند، می‌توانید این مقصدها را بااستفاده از addDestination() به Kotlin DSL خود اضافه کنید:

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

به‌عنوان جایگزین، می‌توانید از عملگر یکانی جمع نیز برای افزودن مقصد جدیداً ساخته‌شده مستقیماً به گراف استفاده کنید:

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

ارائه متغیرهای مستقل مقصد

آرگومان‌های مقصد را می‌توان به‌عنوان بخشی از کلاس مسیر تعریف کرد. این‌ها را می‌توان به همان روشی که برای هر کلاس Kotlin تعریف می‌کنید، تعریف کرد. متغیرهای مستقل الزامی به‌عنوان انواع غیرقابل‌تهی تعریف می‌شوند و متغیرهای مستقل اختیاری با مقادیر پیش‌فرض تعریف می‌شوند.

سازوکار اساسی برای نمایش مسیرها و آرگومان‌های آن‌ها مبتنی بر رشته است. استفاده از رشته‌ها برای مدل‌سازی مسیرها امکان ذخیره و بازیابی وضعیت پیمایش از دیسک را درطول تغییرات پیکربندی و مرگ پردازش آغازشده توسط سیستم فراهم می‌کند. به همین دلیل، هر آرگومان پیمایش باید قابل‌سریال‌سازی باشد، یعنی باید روشی داشته باشد که نمایش درون‌حافظه‌ای مقدار آرگومان را به String تبدیل کند.

افزونه سریال‌سازی Kotlin به‌طور خودکار روش‌های سریال‌سازی را برای انواع پایه تولید می‌کند، وقتی که حاشیه‌نویسی @Serializable به یک شیء اضافه می‌شود.

@Serializable
data class MyRoute(
  val id: String,
  val myList: List<Int>,
  val optionalArg: String? = null
)

fragment<MyFragment, MyRoute>

ارائه انواع سفارشی

برای انواع متغیر مستقل سفارشی، باید کلاس NavType سفارشی ارائه دهید. این به شما امکان می‌دهد دقیقاً کنترل کنید که نوع شما چگونه از مسیر یا پیوند عمیق تجزیه می‌شود.

برای مثال، مسیری که برای تعریف صفحه جستجو استفاده می‌شود می‌تواند حاوی کلاسی باشد که پارامترهای جستجو را نشان می‌دهد:

@Serializable
data class SearchRoute(val parameters: SearchParameters)

@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, SearchRoute>(
    typeMap = mapOf(typeOf<SearchParameters>() to SearchParametersType)
) {
    label = getString(R.string.plant_search_title)
}

هنگام ناوبری به مقصد، نمونه‌ای از مسیرتان ایجاد کنید:

val params = SearchParameters("rose", listOf("available"))
navController.navigate(route = SearchRoute(params))

پارامتر را می‌توان از مسیر در مقصد دریافت کرد:

val searchRoute = navController().getBackStackEntry<SearchRoute>().toRoute<SearchRoute>()
val params = searchRoute.parameters

پیوندهای عمیق

پیوندهای عمیق را می‌توان به هر مقصدی اضافه کرد، درست همان‌طور که می‌توان با نمودار پیمایش XML-محور انجام داد. همه رویه‌های تعریف‌شده در ایجاد پیوند عمیق برای مقصد برای فرایند ایجاد پیوند عمیق بااستفاده از Kotlin DSL اعمال می‌شود.

بااین‌حال، هنگام ایجاد پیوند عمیق ضمنی، منبع پیمایش XML ندارید که بتوان آن را برای عناصر <deepLink> تجزیه‌وتحلیل کرد. بنابراین، نمی‌توانید به قرار دادن عنصر <nav-graph> در فایل AndroidManifest.xml خود تکیه کنید و باید به‌جای آن، فیلترهای قصد را به‌صورت دستی به فعالیت خود اضافه کنید. فیلتر قصدی که ارائه می‌کنید باید با مسیر پایه، کنش، و mimetype پیوندهای عمیق برنامه‌تان مطابقت داشته باشد.

پیوندهای عمیق با فراخوانی تابع deepLink در لامبدای مقصد به مقصد اضافه می‌شوند. مسیر را به‌عنوان نوع پارامتری‌شده و پارامتر basePath را برای مسیر پایه نشانی وب استفاده‌شده برای پیوند عمیق می‌پذیرد.

همچنین می‌توانید بااستفاده از deepLinkBuilder لامبدا دنباله‌دار، کنش و mimetype اضافه کنید.

مثال زیر یک شناسه منبع یکسان پیوند عمیق برای مقصد Home ایجاد می‌کند.

@Serializable data object Home

fragment<HomeFragment, Home>{
  deepLink<Home>(basePath = "www.example.com/home"){
    // Optionally, specify the action and/or mime type that this destination
    // supports
    action = "android.intent.action.MY_ACTION"
    mimeType = "image/*"
  }
}

قالب URI

قالب URI پیوند عمیق به‌طور خودکار از فیلدهای مسیر بااستفاده از قوانین زیر تولید می‌شود:

  • پارامترهای الزامی به‌عنوان پارامترهای مسیر اضافه می‌شوند (مثال: /{id})
  • پارامترهای دارای مقدار پیش‌فرض (پارامترهای اختیاری) به‌عنوان پارامترهای پُرسمان اضافه می‌شوند (مثال: ?name={name})
  • مجموعه‌ها به‌عنوان پارامترهای پُرسمان ضمیمه می‌شوند (مثال: ?items={value1}&items={value2})
  • ترتیب پارامترها با ترتیب فیلدها در مسیر مطابقت دارد

برای مثال، نوع مسیر زیر:

@Serializable data class PlantDetail(
  val id: String,
  val name: String,
  val colors: List<String>,
  val latinName: String? = null,
)

دارای قالب URI تولیدشده زیر است:

basePath/{id}/{name}/?colors={color1}&colors={color2}&latinName={latinName}

محدودیتی برای تعداد پیوندهای عمیقی که می‌توانید اضافه کنید وجود ندارد. هر بار که deepLink() را فراخوانی می‌کنید، پیوند عمیق جدیدی به فهرستی که برای آن مقصد نگهداری می‌شود اضافه می‌شود.

محدودیت‌ها

افزونه Safe Args با Kotlin DSL ناسازگار است، زیرا این افزونه برای تولید کلاس‌های Directions و Arguments به‌دنبال فایل‌های منبع XML می‌گردد.