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

يوفر مكوِّن التنقل لغة خاصة بالنطاق تستند إلى Kotlin أو DSL والتي تعتمد على أدوات الإنشاء المناسبة للنوع في Kotlin. تسمح لك واجهة برمجة التطبيقات هذه بإنشاء الرسم البياني في كود Kotlin، بدلاً من إنشاء مورد XML. يمكن أن يكون هذا مفيدًا إذا كنت تريد إنشاء تنقل تطبيقك بشكل ديناميكي. على سبيل المثال، قد ينزّل تطبيقك إعدادات التنقّل ويخزّنها مؤقتًا من خدمة ويب خارجية، ثم يستخدم تلك الإعدادات لإنشاء رسم بياني للتنقّل بشكل ديناميكي في دالة onCreate() ضمن نشاطك.

التبعيات

لاستخدام Kotlin DSL، أضِف التبعية التالية إلى ملف build.gradle في تطبيقك:

Groovy

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

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

وجهة النشاط

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

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)

بدلاً من ذلك، يمكنك أيضًا استخدام عامل التشغيل الأحادي لإضافة وجهة تم إنشاؤها حديثًا إلى الرسم البياني مباشرةً:

// 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 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 أكثر تحديدًا لكل وجهة مرتبطة بصفحة معيّنة بشكل فردي باستخدام الدالة 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}"
   })
}

يمكنك استخدام استكمال السلسلة لتبسيط التعريف.

القيود

إنّ المكوّن الإضافي فارغات الأمان غير متوافق مع لغة Kotlin DSL، لأنّ المكوّن الإضافي يبحث عن ملفات موارد XML لإنشاء فئتَين Directions وArguments.

مزيد من المعلومات

يمكنك الاطّلاع على صفحة أمان نوع التنقل للتعرّف على كيفية توفير أمان الكتابة لرمز Kotlin DSL ورمز التنقل Compose.