إنشاء رسم بياني آليًا باستخدام لغة Kotlin DSL

يوفّر مكوّن التنقل لغة خاصة بالنطاق تستند إلى Kotlin، والتي تعتمد على أدوات الإنشاء الآمنة للنوع من Kotlin. تتيح لك واجهة برمجة التطبيقات هذه إنشاء رسم بياني بشكلٍ صريح في رمز 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.

يقدّم تطبيق "دوار الشمس" وجهتَين، مع إجراء يربط بينهما.
الشكل 1. يقدّم تطبيق 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، يعمل الإجراء على ربط رقم تعريف الوجهة بوسيطة واحدة أو أكثر. ومع ذلك، عند استخدام DSL للتنقل، يمكن أن يحتوي المسار على وسيطات كجزء من المسار. وهذا يعني عدم وجود مفهوم للإجراءات عند استخدام DSL.

الخطوة التالية هي تحديد بعض الثوابت التي ستستخدمها عند تحديد الرسم البياني.

إنشاء ثوابت للرسم البياني

يتم تحليل الرسوم البيانية للتنقّل في ملف XML كجزء من عملية إنشاء Android. يتم إنشاء ثابت رقمي لكل سمة id محدّدة في الرسم البياني. لا تتوفّر أرقام التعريف الثابتة هذه التي يتم إنشاؤها لوقت الإصدار عند إنشاء الرسم البياني للتنقل في وقت التشغيل، لذا يستخدم تقرير 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() دالة أداة إنشاء DSL. تتطلب هذه الدالة سلسلة مسار للوجهة التي يتم الحصول عليها من الثوابت. تقبل الدالة أيضًا دالة lambda الاختيارية للضبط الإضافي، مثل تسمية الوجهة، فضلاً عن دوال الإنشاء المُضمَّنة للوسيطات والروابط لصفحات في التطبيق.

يتم تمرير الفئة 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 حيث يمكنك توفير إعدادات إضافية كما هو موضّح في قسم التنقل باستخدام الرسم البياني للوحة بيانات Kotlin DSL.

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

وجهة النشاط

تأخذ دالة activity() DSL سلسلة مسار فريدة لتخصيصها لهذه الوجهة، ولكن لا يتم ضبط معلَمة لها لأي فئة نشاط تنفيذ. بدلاً من ذلك، يمكنك ضبط activityClass اختيارية في دالة lambda اللاحقة. تسمح لك هذه المرونة بتحديد وجهة نشاط لنشاط يجب إطلاقه باستخدام هدف ضمني، حيث لا يكون لفئة نشاط فاضح أي معنى. كما هو الحال مع الوجهات المجزأة، يمكنك أيضًا ضبط تصنيف ووسيطات وروابط لصفحات في التطبيق.

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

   activityClass = ActivityDestination::class
}

يمكن استخدام الدالة navigation() DSL لإنشاء رسم بياني تنقّل مدمج. تستخدم هذه الدالة ثلاثة وسيطات: مسار لتعيينه للرسم البياني، ومسار وجهة البداية للرسم البياني، وlambda لإعداد الرسم البياني بصورة أكبر. وتشمل العناصر الصالحة الوجهات الأخرى والوسيطات والروابط لصفحات معيّنة وتصنيفًا وصفيًا للوجهة. ويمكن أن يكون هذا التصنيف مفيدًا لربط الرسم البياني للتنقل بمكونات واجهة المستخدم باستخدام التنقلUI

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

إتاحة الوجهات المخصّصة

إذا كنت تستخدم نوع وجهة جديد لا يتوافق بشكل مباشر مع Kotlin DSL، يمكنك إضافة هذه الوجهات إلى Kotlin باستخدام الرمز 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، وهي الفئة الأساسية لجميع أنواع أدوات إنشاء الوجهات. تأخذ هذه الدالة اسم الوسيطة كسلسلة، ودالة lambda التي تُستخدَم لإنشاء 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 parseValue(value: String): SearchParameters {
    return Json.decodeFromString<SearchParameters>(value)
  }

  // Only required when using Navigation 2.4.0-alpha07 and lower
  override val name = "SearchParameters"
}

ويمكن بعد ذلك استخدام هذا في 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())
    }
}

يستخدم هذا المثال تسلسل Kotlin لتحليل القيمة من السلسلة، وهو ما يعني أنّه يجب استخدام تسلسل Kotlin أيضًا عند الانتقال إلى الوجهة للتأكّد من مطابقة التنسيقات:

val params = SearchParameters("rose", listOf("available"))
val searchArgument = Uri.encode(Json.encodeToString(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 أكثر تحديدًا لكل وجهة مرتبطة بصفحات معيّنة في التطبيق بشكل فردي باستخدام دالة deepLink() DSL. تقبل هذه الدالة NavDeepLink يحتوي على String يمثّل نمط URI، وString يمثّل إجراءات الغرض، و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 ورمز التنقل Compose.