كتابة الأمان في Kotlin DSL والتنقل Compose

تحتوي هذه الصفحة على أفضل الممارسات لتوفير أمان نوع وقت التشغيل لكل من Navigation Kotlin DSL وNavigation Compose. باختصار، يجب ربط كل شاشة في تطبيقك أو الرسم البياني للتنقل بملف تنقل على أساس كل وحدة. يجب أن يحتوي كل ملف من الملفات الناتجة على جميع المعلومات المتعلقة بالتنقل للوجهة المحددة. في ملفات التنقّل هذه أيضًا، توفّر معدّلات رؤية Kotlin الأمان في نوع وقت التشغيل:

  • يتم عرض دوال الكتابة الآمنة بشكل علني لبقية قاعدة الترميز.
  • يتم تحديد المفاهيم الخاصة بالتنقل لشاشة معينة أو رسم بياني للتنقل في موقع مشترك ويتم الحفاظ على خصوصيتها في نفس الملف بحيث لا يمكن الوصول إليها من خلال بقية قاعدة التعليمات البرمجية.

تقسيم الرسم البياني للتنقل

يجب تقسيم الرسم البياني للتنقل حسب الشاشة. هذا هو النهج ذاته في الأساس الذي يتبعه عند تقسيم الشاشات إلى دوال مختلفة قابلة للتكوين. ويجب أن تحتوي كل شاشة على وظيفة إضافة NavGraphBuilder.

تُعد دالة التمديد هذه بمثابة جسر بين دالة قابلة للتكوين على مستوى الشاشة عديمة الحالة والمنطق الخاص بالتنقل. يمكن لهذه الطبقة أيضًا تحديد مصدر الحالة وكيفية التعامل مع الأحداث.

إليك ConversationScreen نموذجي يمكن إضافته إلى internal في وحدته الخاصة، حتى لا تتمكّن الوحدات الأخرى من الوصول إليه:

// ConversationScreen.kt

@Composable
internal fun ConversationScreen(
  uiState: ConversationUiState,
  onPinConversation: () -> Unit,
  onNavigateToParticipantList: (conversationId: String) -> Unit
) { ... }

تعمل دالة الإضافة NavGraphBuilder التالية على إضافة ConversationScreen العنصر كوجهة لذلك NavGraph. كما أنه يربط الشاشة بـ ViewModel الذي يوفر حالة واجهة مستخدم الشاشة ويعالج منطق الأعمال المرتبط بالشاشة. يتم عرض أحداث التنقل التي لا يمكن التعامل معها باستخدام ViewModel للمتصل.

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

// Adds conversation screen to `this` NavGraphBuilder
fun NavGraphBuilder.conversationScreen(
  // Navigation events are exposed to the caller to be handled at a higher level
  onNavigateToParticipantList: (conversationId: String) -> Unit
) {
  composable("conversation/{$conversationIdArg}") {
    // The ViewModel as a screen level state holder produces the screen
    // UI state and handles business logic for the ConversationScreen
    val viewModel: ConversationViewModel = hiltViewModel()
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    ConversationScreen(
      uiState,
      ::viewModel.pinConversation,
      onNavigateToParticipantList
    )
  }
}

يفصل الملف ConversationNavigation.kt الرمز البرمجي عن مكتبة التنقل عن الوجهة نفسها. كما أنه يوفر تغليفًا حول مفاهيم التنقل مثل المسارات أو معرفات الوسيطات التي يتم الحفاظ عليها خاصة لأنه يجب عدم تسريبها أبدًا خارج هذا الملف. يجب أن يتم عرض أحداث التنقل التي لا يمكن التعامل معها في هذه الطبقة للمتصل حتى يتم التعامل معها على المستوى الصحيح. يمكنك العثور على مثال لهذا الحدث باستخدام onNavigateToParticipantList في مقتطف الرمز أعلاه.

كتابة "التنقل الآمن"

إنّ التنقل Kotlin DSL الذي تستند إليه ميزة Navigation Compose لا يوفّر حاليًا أمان نوع وقت التجميع الذي يوفّره نوع Safe Args، للرسومات البيانية للتنقّل المضمّنة في ملفات موارد XML للتنقل. تنشئ Safe Args رمزًا برمجيًا يحتوي على فئات وطرق آمنة لوجهات وإجراءات التنقل. ومع ذلك، يمكنك هيكلة رمز التنقل بحيث يكون كتابة آمنة في وقت التشغيل. باستخدام هذه الميزة، يمكنك تجنُّب الأعطال والتأكّد من إجراء ما يلي:

  • الوسيطات التي تقدمها عند الانتقال إلى رسم بياني للوجهة أو التنقل هي الأنواع الصحيحة وأن جميع الوسيطات المطلوبة موجودة.
  • الوسائط التي تستردها من SavedStateHandle هي الأنواع الصحيحة.

يجب أن تعرض كل وجهة أيضًا وظيفة الإضافة NavController للسماح للوجهات الأخرى بالانتقال إليها بأمان.

// ConversationNavigation.kt

fun NavController.navigateToConversation(conversationId: String) {
    this.navigate("conversation/$conversationId")
}

إذا أردت الانتقال إلى شاشة تطبيقك باستخدام NavOptions مختلفة، مثل popUpTo, savedState, restoreState أو singleTop، عند التنقّل، اضبط معلَمة اختيارية على دالة الإضافة NavController.

// HomeNavigation.kt

const val HomeRoute = "home"

fun NavController.navigateToHome(navOptions: NavOptions? = null) {
    this.navigate(HomeRoute, navOptions)
}

كتابة برنامج تضمين الوسيطات الآمنة

يمكنك اختياريًا إنشاء برنامج تضمين آمن لنوع لاستخراج الوسيطات من SavedStateHandle الخاصة بك ViewModel ومن NavBackStackEntry في محتوى الوجهة، وذلك للحصول على المزايا المذكورة في مقدمة هذا القسم.

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

internal class ConversationArgs(val conversationId: String) {
  constructor(savedStateHandle: SavedStateHandle) :
    this(checkNotNull(savedStateHandle[conversationIdArg]) as String)
}

// ConversationViewModel.kt

internal class ConversationViewModel(...,
  savedStateHandle: SavedStateHandle
) : ViewModel() {
  private val conversationArgs = ConversationArgs(savedStateHandle)
}

تجميع الرسم البياني للتنقل

تستخدم الرسوم البيانية للتنقل دوال نوع الإضافة الآمنة الموضحة أعلاه لإضافة الوجهات والانتقال إليها.

في المثال التالي، يتم تضمين وجهة المحادثة مع وجهتين أخريين، الصفحة الرئيسية وقائمة المشاركين، في مستوى التطبيق NavHost على النحو التالي:

// MyApp.kt

@Composable
fun MyApp(modifier: Modifier = Modifier) {
  val navController = rememberNavController()
  NavHost(
    navController = navController,
    startDestination = HomeRoute,
    modifier = modifier
  ) {

    homeScreen(
      onNavigateToConversation = { conversationId ->
        navController.navigateToConversation(conversationId)
      }
    )

    conversationScreen(
      onNavigateToParticipantList = { conversationId ->
        navController.navigateToParticipantList(conversationId)
      }
    }

    participantListScreen()
}

كتابة الأمان في الرسوم البيانية المدمجة للتنقّل

يجب اختيار مستوى الرؤية المناسب للوحدات التي توفّر شاشات متعدّدة. وهذا هو المفهوم نفسه المتّبع في كل طريقة واردة في الأقسام أعلاه. ومع ذلك، قد لا يكون من المنطقي أن تعرض الشاشات الفردية لوحدات أخرى على الإطلاق. في هذه الحالة، يجب عليك بدلاً من ذلك أن تعاملها كجزء من تدفق أكبر ومستقل.

تُعرف هذه المجموعة المستقلة من الشاشات باسم رسم بياني للتنقل المدمج. يتيح لك ذلك تضمين شاشات متعددة في طريقة واحدة لإضافة NavGraphBuilder. وتستخدم هذه الطريقة طرق الإضافة NavController هذه بالترتيب لربط الشاشات المتوفّرة في الوحدة نفسها.

في المثال التالي، تظهر وجهة المحادثة الموضحة في الأقسام السابقة في رسم بياني مدمج للتنقّل إلى جانب وجهتين أخريين، وهما قائمة المحادثات وقائمة المشاركين:

// ConversationGraphNavigation.kt

private val ConversationGraphRoutePattern = "conversation"

fun NavController.navigateToConversationGraph(navOptions: NavOptions? = null) {
  this.navigate(ConversationGraphRoutePattern, navOptions)
}

fun NavGraphBuilder.conversationGraph(navController: NavController) {
  navigation(
    startDestination = ConversationListRoutePattern,
    route = ConversationGraphRoutePattern
  ) {
    conversationListScreen(
      onNavigateToConversation = { conversationId ->
        navController.navigateToConversation(conversationId)
      }
    )
    conversationScreen(
      onNavigateToParticipantList = { conversationId ->
        navController.navigateToParticipantList(conversationId)
      }
    )
    partipantList()
}

يمكنك استخدام العديد من الرسوم البيانية المدمَجة للتنقّل في مستوى التطبيق NavHost على النحو التالي:

// MyApp.kt

@Composable
fun MyApp(modifier: Modifier = Modifier) {
  val navController = rememberNavController()
  NavHost(
    navController = navController,
    startDestination = HomeGraphRoutePattern
    modifier = modifier
  ) {
    homeGraph(
      navController,
      onNavigateToConversation = {
        navController.navigateToConversationGraph()
      }
    }
    conversationGraph(navController)
}