با استفاده از Kotlin DSL یک گراف بسازید

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

وابستگی ها

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

شیار

dependencies {
    def nav_version = "2.7.7"

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

کاتلین

dependencies {
    val nav_version = "2.7.7"

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

ساختن نمودار

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

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

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

میزبانی کاتلین DSL Nav Graph

قبل از اینکه بتوانید نمودار ناوبری برنامه خود را بسازید، به مکانی برای میزبانی نمودار نیاز دارید. این مثال از قطعات استفاده می کند، بنابراین گراف را در 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 به عنوان بخشی از فرآیند ساخت اندروید تجزیه می شوند. یک ثابت عددی برای هر ویژگی id تعریف شده در نمودار ایجاد می شود. این شناسه‌های استاتیک تولید شده در زمان ساخت، هنگام ساخت نمودار ناوبری شما در زمان اجرا در دسترس نیستند، بنابراین Navigation 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
        }
    }
}

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

کلاس Fragment که رابط کاربری هر مقصد را مدیریت می کند به عنوان یک نوع پارامتری در داخل براکت های زاویه ( <> ) ارسال می شود. این کار مانند تنظیم ویژگی android:name در مقصدهای قطعه ای که با استفاده از XML تعریف شده اند، دارد.

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

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 را می توان به کلاس Fragment پیاده سازی پارامتربندی کرد و یک رشته مسیر منحصر به فرد را برای تخصیص به این مقصد انتخاب کرد، به دنبال آن یک lambda که در آن می توانید پیکربندی اضافی را همانطور که در بخش Navigating with Kotlin DSL graph توضیح داده شده است، ارائه دهید.

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

مقصد فعالیت

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

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

   activityClass = ActivityDestination::class
}

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

navigation("route_to_this_graph", nav_routes.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)

به عنوان یک جایگزین، شما همچنین می توانید از عملگر unary plus برای اضافه کردن یک مقصد جدید به طور مستقیم به نمودار استفاده کنید:

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

ارائه آرگومان های مقصد

هر مقصدی می تواند آرگومان هایی را تعریف کند که اختیاری یا ضروری هستند. اقدامات را می توان با استفاده از تابع argument() در NavDestinationBuilder تعریف کرد که کلاس پایه برای همه انواع سازنده مقصد است. این تابع نام آرگومان را به عنوان یک رشته و یک لامبدا می گیرد که برای ساخت و پیکربندی یک NavArgument استفاده می شود.

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

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 Serialization یا سایر کتابخانه ها برای ارائه کدگذاری و رمزگشایی بدون بازتاب نوع سفارشی خود استفاده کنید.

به عنوان مثال، یک کلاس داده که پارامترهای جستجو را نشان می دهد که به صفحه جستجوی شما منتقل می شود، می تواند 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)

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

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

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

شما می توانید با استفاده از تابع deepLink() DSL، برای هر مقصد پیوند عمیق جداگانه، یک deeplink خاص ارائه کنید. این تابع یک 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 و کد ناوبری خود ایجاد کنید.