دليل: نقل البيانات إلى Type-Safe Navigation في Compose وNavigation 2

يوضّح هذا الدليل عملية استبدال المسارات المستندة إلى السلاسل بأنواع Kotlin قابلة التسلسل لتحقيق الأمان في وقت الترجمة وإزالة الأعطال في وقت التشغيل الناتجة عن أخطاء إملائية أو أنواع وسيطات غير صحيحة.

المتطلّبات الأساسية

قبل بدء عملية النقل، تأكَّد من أنّ مشروعك يستوفي المتطلبات التالية:

  1. إصدار Navigation: يجب التحديث إلى الإصدار 2.8.0 من Jetpack Navigation أو إصدار أحدث
  2. المكوّن الإضافي لتسلسل Kotlin:
  3. أضِف المكوّن الإضافي إلى libs.versions.toml:
[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
  • أضِف التبعيات إلى ملف build.gradle.kts على أعلى مستوى وملف build.gradle.kts على مستوى الوحدة.

الخطوة 1: تحديد الوجهات

استبدِل سلاسل المسارات الثابتة بكائنات وفئات @Serializable.

  • بالنسبة إلى الشاشات التي لا تتضمّن وسيطات: استخدِم data object
  • بالنسبة إلى الشاشات التي تتضمّن وسيطات: استخدِم data class

قبل (استنادًا إلى السلسلة):

const val ROUTE_HOME = "home"
const val ROUTE_PROFILE = "profile/{userId}"

بعد (آمن من حيث النوع):

import kotlinx.serialization.Serializable

@Serializable
object Home

@Serializable
data class Profile(val userId: String)

الخطوة 2: تعديل إعدادات NavHost

حدِّث NavHost لاستخدام الأنواع العامة الجديدة في الدالتَين composable وdialog.

قبل:

NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(...) }
    composable("profile/{userId}") { backStackEntry ->
        val userId = backStackEntry.arguments?.getString("userId")
        ProfileScreen(userId)
    }
}

بعد:

NavHost(navController, startDestination = Home) {
    composable<Home> {
        HomeScreen(...)
    }
    composable<Profile> { backStackEntry ->
        // The library automatically handles argument extraction
        val profile: Profile = backStackEntry.toRoute()
        ProfileScreen(profile.userId)
    }
}

الخطوة 3: تنفيذ طلبات التنقّل الآمنة من حيث النوع

استبدِل طلبات التنقّل التي تم إدخالها في السلسلة بمثيلات الفئات.

قبل:

navController.navigate("profile/user123")

بعد:

navController.navigate(Profile(userId = "user123"))

الخطوة 4: الوصول إلى الوسيطات في ViewModels

في حال استخدام ViewModel، يمكنك الآن استخراج عنصر المسار مباشرةً من SavedStateHandle.

التنفيذ:

class ProfileViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    // Automatically parses arguments into the Profile class
    private val profile = savedStateHandle.toRoute<Profile>()
    val userId = profile.userId
}

الخطوة 5: (متقدّمة) التعامل مع الأنواع المخصّصة

إذا كنت بحاجة إلى تمرير فئات بيانات معقّدة (وليس فقط أنواع البيانات الأساسية)، عليك تحديد NavType مخصّص.

  1. إنشاء النوع المخصّص: ```kotlin val SearchFilterType = object : NavType(isNullableAllowed = false) { override fun get(bundle: Bundle, key: String): SearchFilter? = Json.decodeFromString(bundle.getString(key) ?: return null)
override fun parseValue(value: String): SearchFilter =
    Json.decodeFromString(Uri.decode(value))

override fun put(bundle: Bundle, key: String, value: SearchFilter) {
    bundle.putString(key, Json.encodeToString(value))
}

}



2. **Register it in the Graph**:
```kotlin
composable<Search>(
    typeMap = mapOf(typeOf<SearchFilter>() to SearchFilterType)
) { ... }

أفضل الممارسات والنصائح

  • التسلسلات الهرمية المحكمة: بالنسبة إلى التطبيقات الكبيرة، يمكنك تجميع مساراتك باستخدام واجهة أو فئة محكمة للحفاظ على تنظيم بنية التنقّل.
  • مثيلات الكائنات: بالنسبة إلى المسارات التي لا تتضمّن مَعلمات، استخدِم object دائمًا بدلاً من class لتجنُّب عمليات التخصيص غير الضرورية.
  • الأنواع التي يمكن أن تقبل القيم الفارغة: تتيح واجهة برمجة التطبيقات الجديدة الأنواع التي يمكن أن تقبل القيم الفارغة (مثل data class Search(val query: String?)) وتوفّر القيم التلقائية تلقائيًا.
  • الاختبار: استخدِم navController.currentBackStackEntry?.hasRoute<T>() للتحقّق من الوجهة الحالية بطريقة آمنة من حيث النوع أثناء اختبارات واجهة المستخدم