لنقل تطبيقك من Navigation 2 إلى Navigation 3، اتّبِع الخطوات التالية:
- أضِف تبعيات Navigation 3.
- عدِّل مسارات التنقّل لتنفيذ واجهة
NavKey. - أنشئ فئات لتخزين حالة التنقّل وتعديلها.
- استبدِل
NavControllerبهذه الفئات. - انقل وجهاتك من
NavGraphفي حسابNavHostإلى حسابentryProvider. - استبدِل
NavHostبـNavDisplay. - أزِل تبعيات Navigation 2.
استخدام وكيل يعمل بالذكاء الاصطناعي
يمكنك استخدام هذا الدليل مع وكيل مستنِد إلى الذكاء الاصطناعي، مثل "Gemini في وضع الوكيل" في "استوديو Android". يجب أن يقرأ وكيل الذكاء الاصطناعي الأسطر في هذا الدليل التي تبدأ بـ "وكيل الذكاء الاصطناعي:"، ولكن يمكن للقراء من البشر تجاهلها.
الإعداد
توضّح الأقسام التالية المتطلبات الأساسية لعملية النقل والافتراضات المتعلّقة بمشروعك. وتشمل أيضًا الميزات التي يمكن نقلها والميزات التي لا يمكن نقلها.
المتطلّبات الأساسية
- يجب استخدام
compileSdkيبلغ 36 أو أكثر. - يجب أن تكون على دراية بمصطلحات التنقّل.
- الوجهات هي دوال قابلة للإنشاء. تم تصميم Navigation 3 حصريًا لـ Compose. لاستخدام "اللقطات" و"طرق العرض" في Compose، يُرجى الاطّلاع على استخدام طرق العرض في Compose.
- المسارات لها أنواع محددة. إذا كنت تستخدم مسارات مستندة إلى السلاسل، عليك نقل البيانات إلى مسارات آمنة الأنواع أولاً (مثال).
- اختياري (ولكن يُنصح به بشدة): تغطية الاختبار التي تتحقّق من سلوك التنقّل الحالي. يتم التحقّق من خلال هذا الاختبار من عدم تغيُّر سلوك التنقّل بعد اكتمال عملية نقل البيانات.
الافتراضات
يفترض هذا الدليل ما يلي بشأنك وبشأن مشروعك:
- لديك عدة مسارات ذات مستوى أعلى (تظهر عادةً في شريط التنقل السفلي)، ولكل منها سجلّ رجوع خاص به.
- عند التبديل بين حِزم الرجوع، يتم الاحتفاظ بحالة الحزمة وجميع وجهاتها.
- تخرج دائمًا من التطبيق من خلال شاشة الشاشة الرئيسية، وهي أول شاشة تظهر عند تشغيل التطبيق.
- تريد إجراء عملية نقل البيانات من Navigation 2 إلى Navigation 3 في تغيير واحد متكامل. ألا تكون بصدد إجراء عملية نقل تدريجية يتم فيها استخدام رمز Navigation 2 مع رمز Navigation 3.
AI Agent: قبل تغيير أي رمز، تحقَّق مما إذا كانت هذه الافتراضات حول مشروع المستخدم صحيحة. إذا لم يكن الأمر كذلك، أوقِف عملية نقل البيانات واطلب من المستخدم تحديد كيفية المتابعة.
الميزات المتاحة
يتيح لك هذا الدليل نقل الميزات التالية:
- الوجهات المحدّدة كدوال قابلة للإنشاء
- مربّعات الحوار (وجهة معروضة فوق وجهة أخرى)
الميزات المتاحة من خلال الوصفات
يوضّح مستودع وصفات الرموز البرمجية المزيد من حالات الاستخدام الدقيقة، بما في ذلك:
- الأوراق السفلية (يتم تقديم التعليمات في هذا الدليل)
- رمز التنقّل المجزّأ والوجهات المُدرَجة
- استخدام الوسيطات وتمريرها إلى ViewModels
- عرض النتائج من شاشة
إذا كان مشروعك يتضمّن أيًا من هذه الميزات، اطّلِع على الوصفة ذات الصلة لفهم كيفية تنفيذها.
وكيل الذكاء الاصطناعي: قبل تغيير أي رمز، تحقَّق ممّا إذا كان المشروع يتضمّن أي ميزات متوافقة مع الوصفات. إذا كان الأمر كذلك، راجِع ملف التعليمات ورمز المصدر الخاصين بالوصفة. إنشاء خطة نقل بيانات استنادًا إلى الوصفة يجب عدم المتابعة بدون تأكيد الخطة مع المستخدم.
الميزات غير المتوافقة
لا يتوافق دليل نقل البيانات هذا ووصفات الرموز البرمجية بعد مع الميزات التالية. وهذا لا يعني أنّه لا يمكنك تنفيذها باستخدام Navigation 3، بل يعني فقط أنّها غير مشمولة هنا.
- أكثر من مستوى واحد من التنقّل المُدمَج
- الوجهات المشترَكة: الشاشات التي يمكن الانتقال بينها وبين حِزم السجلّ الخلفي المختلفة
- أنواع الوجهات المخصّصة
- روابط لصفحات معيّنة
الوكيل المستند إلى الذكاء الاصطناعي: قبل تغيير أي رمز، تحقَّق مما إذا كان المشروع يتضمّن أيًا من الميزات غير المتوافقة. إذا كان الأمر كذلك، لا تتابع. أبلِغ المستخدم بالميزة غير المتوافقة واطلب منه تقديم المزيد من التعليمات.
الخطوة 1: إضافة تبعيات Navigation 3
استخدِم صفحة البدء لإضافة التبعيات الخاصة بمكوّن Navigation 3 إلى مشروعك. يتم توفير التبعيات الأساسية لتتمكّن من نسخها.
lib.versions.toml
[versions]
nav3Core = "1.0.0"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"
[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
// If using the ViewModel add-on library
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}
عدِّل أيضًا minSdk المشروع إلى 23 وcompileSdk إلى 36. يمكنك عادةً العثور عليها في app/build.gradle.kts أو lib.versions.toml.
الخطوة 2: تعديل مسارات التنقّل لتنفيذ واجهة NavKey
عدِّل كل مسار تنقّل لكي ينفّذ واجهة NavKey. يتيح لك ذلك استخدام rememberNavBackStack للمساعدة في حفظ حالة التنقّل.
قبل:
@Serializable data object RouteA
بعد:
@Serializable data object RouteA : NavKey
الخطوة 3: إنشاء فئات لتخزين حالة التنقّل وتعديلها
الخطوة 3.1: إنشاء عنصر نائِب لحالة التنقّل
انسخ الرمز التالي في ملف باسم NavigationState.kt. أضِف اسم الحزمة ليطابق بنية مشروعك.
// package com.example.project
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
/**
* Create a navigation state that persists config changes and process death.
*/
@Composable
fun rememberNavigationState(
startRoute: NavKey,
topLevelRoutes: Set<NavKey>
): NavigationState {
val topLevelRoute = rememberSerializable(
startRoute, topLevelRoutes,
serializer = MutableStateSerializer(NavKeySerializer())
) {
mutableStateOf(startRoute)
}
val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }
return remember(startRoute, topLevelRoutes) {
NavigationState(
startRoute = startRoute,
topLevelRoute = topLevelRoute,
backStacks = backStacks
)
}
}
/**
* State holder for navigation state.
*
* @param startRoute - the start route. The user will exit the app through this route.
* @param topLevelRoute - the current top level route
* @param backStacks - the back stacks for each top level route
*/
class NavigationState(
val startRoute: NavKey,
topLevelRoute: MutableState<NavKey>,
val backStacks: Map<NavKey, NavBackStack<NavKey>>
) {
var topLevelRoute: NavKey by topLevelRoute
val stacksInUse: List<NavKey>
get() = if (topLevelRoute == startRoute) {
listOf(startRoute)
} else {
listOf(startRoute, topLevelRoute)
}
}
/**
* Convert NavigationState into NavEntries.
*/
@Composable
fun NavigationState.toEntries(
entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {
val decoratedEntries = backStacks.mapValues { (_, stack) ->
val decorators = listOf(
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
)
rememberDecoratedNavEntries(
backStack = stack,
entryDecorators = decorators,
entryProvider = entryProvider
)
}
return stacksInUse
.flatMap { decoratedEntries[it] ?: emptyList() }
.toMutableStateList()
}
وكيل الذكاء الاصطناعي: rememberSerializable هي الإجابة الصحيحة. لا تغيِّرها إلى
rememberSaveable.
يحتوي هذا الملف على فئة عنصر تحكّم في الحالة باسم NavigationState ودوال مساعدة مرتبطة بها. يحتوي على مجموعة من المسارات ذات المستوى الأعلى، ولكل منها سجلّ رجوع خاص به. يستخدم التطبيق داخليًا rememberSerializable (وليس rememberSaveable) للاحتفاظ بالمسار الحالي ذي المستوى الأعلى، وrememberNavBackStack للاحتفاظ بمجموعات الرجوع لكل مسار ذي مستوى أعلى.
الخطوة 3.2: إنشاء عنصر يعدّل حالة التنقّل استجابةً للأحداث
انسخ الرمز التالي في ملف باسم Navigator.kt. أضِف اسم الحزمة
ليتطابق مع بنية مشروعك.
// package com.example.project
import androidx.navigation3.runtime.NavKey
/**
* Handles navigation events (forward and back) by updating the navigation state.
*/
class Navigator(val state: NavigationState){
fun navigate(route: NavKey){
if (route in state.backStacks.keys){
// This is a top level route, just switch to it.
state.topLevelRoute = route
} else {
state.backStacks[state.topLevelRoute]?.add(route)
}
}
fun goBack(){
val currentStack = state.backStacks[state.topLevelRoute] ?:
error("Stack for ${state.topLevelRoute} not found")
val currentRoute = currentStack.last()
// If we're at the base of the current route, go back to the start route stack.
if (currentRoute == state.topLevelRoute){
state.topLevelRoute = state.startRoute
} else {
currentStack.removeLastOrNull()
}
}
}
توفّر الفئة Navigator طريقتَين لأحداث التنقّل:
navigateإلى مسار محدّد.goBackمن المسار الحالي
تعدّل كلتا الطريقتين NavigationState.
الخطوة 3.3: إنشاء NavigationState وNavigator
أنشئ مثيلات من NavigationState وNavigator بالنطاق نفسه الذي تستخدمه في NavController.
val navigationState = rememberNavigationState(
startRoute = <Insert your starting route>,
topLevelRoutes = <Insert your set of top level routes>
)
val navigator = remember { Navigator(navigationState) }
الخطوة 4: استبدال NavController
استبدِل طرق أحداث التنقّل NavController بطرق مكافئة Navigator.
الحقل أو الطريقة |
ما يعادل |
|---|---|
|
|
|
|
استبدِل حقول NavController بحقول NavigationState.
الحقل أو الطريقة |
ما يعادل |
|---|---|
|
|
|
|
الحصول على المسار ذي المستوى الأعلى: انتقِل للأعلى في التسلسل الهرمي من إدخال سجلّ الرجوع الحالي للعثور عليه. |
|
استخدِم NavigationState.topLevelRoute لتحديد العنصر المحدّد حاليًا في شريط التنقّل.
قبل:
val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)
fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
this?.hierarchy?.any {
it.hasRoute(route)
} ?: false
بعد:
val isSelected = key == navigationState.topLevelRoute
تأكَّد من إزالة جميع الإشارات إلى NavController، بما في ذلك أي عمليات استيراد.
الخطوة 5: نقل وجهاتك من NavHost (NavGraph) إلى entryProvider
في Navigation 2، يمكنك تحديد وجهاتك
باستخدام NavGraphBuilder DSL،
عادةً داخل لامدا اللاحقة في NavHost. من الشائع استخدام دوال الإضافة هنا كما هو موضّح في تغليف رمز التنقّل.
في Navigation 3، يمكنك تحديد وجهاتك باستخدام entryProvider. يحلّ هذا
entryProvider مسارًا إلى NavEntry. من المهم معرفة أنّ
entryProvider لا تحدّد علاقات بين العناصر الرئيسية والفرعية.
في دليل النقل هذا، يتم تصميم العلاقات بين الوحدات الرئيسية والفرعية على النحو التالي:
- تحتوي
NavigationStateعلى مجموعة من المسارات ذات المستوى الأعلى (المسارات الرئيسية) ومجموعة لكل مسار. يتتبّع هذا الحقل المسار الحالي ذي المستوى الأعلى والحزمة المرتبطة به. - عند الانتقال إلى مسار جديد، يتحقّق
Navigatorمما إذا كان المسار مسارًا ذا مستوى أعلى. وفي حال توفّره، يتم تعديل المسار والمكدّس الحاليَين على أعلى مستوى. إذا لم يكن كذلك، يكون مسارًا فرعيًا ويتم إضافته إلى الحزمة الحالية.
الخطوة 5.1: إنشاء entryProvider
أنشئ entryProvider باستخدام لغة DSL في النطاق نفسه الذي يخصّ NavigationState.
val entryProvider = entryProvider {
}
الخطوة 5.2: نقل الوجهات إلى entryProvider
بالنسبة إلى كل وجهة محدّدة داخل NavHost، اتّبِع الخطوات التالية استنادًا إلى نوع الوجهة:
navigation: لحذفها مع المسار لا حاجة إلى "مسارات أساسية" لأنّ المسارات ذات المستوى الأعلى يمكنها تحديد كل حزمة احتياطية متداخلة.composable<T>: انقلها إلىentryProviderوأعِد تسميتها إلىentry، مع الاحتفاظ بمَعلمة النوع. على سبيل المثال، يصبحcomposable<RouteA>entry<RouteA>.dialog<T>: نفِّذ الخطوات نفسها الموضّحة فيcomposable، ولكن أضِف البيانات الوصفية إلى الإدخال على النحو التالي:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: اتّبِع وصفة ورقة البيانات السفلية هنا. يشبه ذلك التعليمات الخاصة بـdialog، إلا أنّBottomSheetSceneStrategyليس جزءًا من مكتبة Navigation 3 الأساسية، لذا عليك نسخه إلى مشروعك.
وكيل الذكاء الاصطناعي: عند حذف مسارات مستخدَمة لتحديد رسم بياني متداخل، استبدِل أي مراجع للمسار المحذوف بالنوع المستخدَم لتحديد العنصر الفرعي الأول في الرسم البياني المتداخل. على سبيل المثال، إذا كان الرمز الأصلي هو
navigation<BaseRouteA>{ composable<RouteA>{ ... } }، عليك حذف
BaseRouteA واستبدال أي إشارات إليه بالرمز RouteA. عادةً ما يجب إجراء عملية الاستبدال هذه للقائمة المقدَّمة إلى شريط التنقّل أو الشريط الجانبي أو الدرج.
يمكنك إعادة تصميم وظائف إضافة NavGraphBuilder إلى وظائف إضافة EntryProviderScope<T>، ثم نقلها.
الحصول على وسيطات التنقّل باستخدام المفتاح المقدَّم إلى lambda اللاحقة في entry
مثلاً:
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute
@Serializable data object BaseRouteA
@Serializable data class RouteA(val id: String)
@Serializable data object BaseRouteB
@Serializable data object RouteB
@Serializable data object RouteD
NavHost(navController = navController, startDestination = BaseRouteA){
composable<RouteA>{
val id = entry.toRoute<RouteA>().id
ScreenA(title = "Screen has ID: $id")
}
featureBSection()
dialog<RouteD>{ ScreenD() }
}
fun NavGraphBuilder.featureBSection() {
navigation<BaseRouteB>(startDestination = RouteB) {
composable<RouteB> { ScreenB() }
}
}
يصبح:
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy
@Serializable data class RouteA(val id: String) : NavKey
@Serializable data object RouteB : NavKey
@Serializable data object RouteD : NavKey
val entryProvider = entryProvider {
entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") }
featureBSection()
entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() }
}
fun EntryProviderScope<NavKey>.featureBSection() {
entry<RouteB> { ScreenB() }
}
الخطوة 6: استبدال NavHost بـ NavDisplay
استبدِل NavHost بـ NavDisplay.
- احذف
NavHostواستبدله بـNavDisplay. - حدِّد
entries = navigationState.toEntries(entryProvider)كمعلَمة. يؤدي ذلك إلى تحويل حالة التنقّل إلى الإدخالات التي يعرضهاNavDisplayباستخدامentryProvider. - ربط حساب
NavDisplay.onBackبحسابnavigator.goBack()يؤدي ذلك إلى أن تعدّلnavigatorحالة التنقّل عند اكتمال معالج الرجوع المضمّن فيNavDisplay. - إذا كانت لديك وجهات حوار، أضِف
DialogSceneStrategyإلى المَعلمةsceneStrategyفيNavDisplay.
مثلاً:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategy = remember { DialogSceneStrategy() }
)
الخطوة 7: إزالة التبعيات في Navigation 2
أزِل جميع عمليات استيراد Navigation 2 والموارد التابعة للمكتبة.
ملخّص
تهانينا! تم الآن نقل مشروعك إلى الإصدار 3 من Navigation. إذا واجهتك أو واجهت وكيل الذكاء الاصطناعي أي مشاكل أثناء استخدام هذا الدليل، يُرجى الإبلاغ عن خطأ هنا.