لنقل تطبيقك من Navigation 2 إلى Navigation 3، اتّبِع الخطوات التالية:
- أضِف الاعتمادات الخاصة بمكتبة Navigation 3.
- عدِّل مسارات التنقّل لتنفيذ واجهة
NavKey. - أنشئ فئات لتخزين حالة التنقّل وتعديلها.
- استبدِل
NavControllerبهذه الفئات. - انقل وجهاتك من
NavGraphفيNavHostإلىentryProvider. - استبدِل
NavHostبـNavDisplay. - إزالة التبعيات في Navigation 2
طلب موجَّه إلى الذكاء الاصطناعي
الترحيل من الإصدار 2 إلى الإصدار 3 من "شريط التنقّل"
سيستخدم هذا الإشعار هذا الدليل للانتقال إلى الإصدار 3 من التنقّل.
Migrate from Navigation 2 to Navigation 3 using the official
migration guide.إذا واجهت مشاكل، يُرجى تسجيل مشكلة هنا.
الإعداد
توضّح الأقسام التالية المتطلبات الأساسية لعملية النقل والافتراضات المتعلّقة بمشروعك. وتشمل أيضًا الميزات المتاحة لنقل البيانات والميزات غير المتاحة.
المتطلبات الأساسية
- يجب استخدام
compileSdk36 أو إصدار أحدث. - يجب أن تكون على دراية بمصطلحات التنقّل.
- الوجهات هي دوال قابلة للإنشاء. تم تصميم Navigation 3 حصريًا لـ Compose. لاستخدام الأجزاء وعناصر View في Compose، راجِع استخدام عناصر View في Compose.
- المسارات مكتوبة بشكل صارم. إذا كنت تستخدم مسارات مستندة إلى السلاسل، عليك نقل البيانات إلى مسارات آمنة من حيث النوع أولاً (مثال).
- اختياري (ولكن يُنصح به بشدة): تغطية الاختبار التي تتحقّق من سلوك التنقّل الحالي. يتم التحقّق باستخدام هذه الطريقة من عدم تغيير سلوك التنقّل بعد إكمال عملية نقل البيانات.
الافتراضات
يفترض هذا الدليل ما يلي بشأنك وبشأن مشروعك:
- لديك مسار واحد أو عدّة مسارات من المستوى الأعلى (تظهر عادةً في شريط التنقّل السفلي)، ولكل منها سجلّ رجوع خاص به.
- عند التبديل بين حِزم الرجوع، يتم الاحتفاظ بحالة الحزمة وجميع وجهاتها.
- تخرج دائمًا من التطبيق من خلال شاشة الشاشة الرئيسية، وهي أول شاشة تظهر عند تشغيل التطبيق.
- تريد إجراء عملية نقل البيانات من Navigation 2 إلى Navigation 3 في تغيير واحد متكامل. ألا تكون بصدد إجراء عملية نقل تدريجية يتم فيها استخدام رمز Navigation 2 مع رمز Navigation 3.
وكيل الذكاء الاصطناعي: قبل تغيير أي رمز، تحقَّق مما إذا كانت هذه الافتراضات حول مشروع المستخدم صحيحة. إذا لم يكن الأمر كذلك، أوقِف عملية نقل البيانات واسأل المستخدم عن كيفية المتابعة.
الميزات المتاحة
يتيح لك هذا الدليل نقل الميزات التالية:
- الوجهات المحدّدة كدوال قابلة للإنشاء
- مربّعات الحوار (وجهة معروضة فوق وجهة أخرى)
الميزات المتاحة من خلال الوصفات
يوضّح مستودع وصفات الرموز البرمجية المزيد من حالات الاستخدام الدقيقة، بما في ذلك:
- الأوراق السفلية (يتم تقديم التعليمات في هذا الدليل)
- رمز التنقّل المجزّأ والوجهات المُدرَجة
- استخدام وسيطات وتمريرها إلى
ViewModel - عرض النتائج من شاشة
إذا كان مشروعك يتضمّن أيًا من هذه الميزات، يمكنك الاطّلاع على الوصفة ذات الصلة للتعرّف على كيفية تنفيذها.
AI Agent: قبل تغيير أي رمز، تحقَّق ممّا إذا كان المشروع يتضمّن أي ميزات متوافقة مع الوصفات. إذا كان الأمر كذلك، راجِع ملف التعليمات ورمز المصدر الخاصين بالوصفة. أنشئ خطة نقل بيانات استنادًا إلى الوصفة. يجب عدم المتابعة بدون تأكيد الخطة مع المستخدم.
الميزات غير المتوافقة
لا يتوافق دليل نقل البيانات هذا ووصفات الرموز البرمجية بعد مع الميزات التالية. لا يعني ذلك أنّه لا يمكنك تنفيذها باستخدام الإصدار 3 من Navigation، بل يعني فقط أنّها غير مشمولة هنا.
- أكثر من مستوى واحد من التنقّل المتداخل
- الوجهات المشترَكة: الشاشات التي يمكن الانتقال بينها وبين حِزم السجلّ الخلفي المختلفة
- أنواع الوجهات المخصّصة
- الروابط العميقة
الوكيل المستند إلى الذكاء الاصطناعي: قبل تغيير أي رمز، تحقَّق مما إذا كان المشروع يتضمّن أيًا من الميزات غير المتوافقة. إذا كان الأمر كذلك، لا تتابع. أبلِغ المستخدم بالميزة غير المتوافقة واطلب منه تقديم المزيد من التعليمات.
الخطوة 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، بما في ذلك أي عمليات استيراد.
الخطوة 4.1: نقل منطق متوافق مع مراحل النشاط
في Navigation 2، تنفّذ NavBackStackEntry LifecycleOwner، ما يتيح لك الاستماع إلى أحداث مراحل النشاط أو جمع التدفقات بطريقة تراعي مراحل النشاط باستخدام navController.currentBackStackEntry.
في الإصدار 3 من Navigation، يوفّر NavDisplay نطاق دخول LifecycleOwner
من خلال LocalLifecycleOwner.current إلى المحتوى القابل للإنشاء لكل وجهة. يمكنك الاطّلاع على مراحل نشاط الوجهة للحصول على مزيد من المعلومات.
يجب تنفيذ العمليات التي تراعي مراحل النشاط مباشرةً داخل المحتوى القابل للإنشاء في وجهتك من خلال الإشارة إلى LocalLifecycleOwner.current.
على سبيل المثال، إذا كنت تجمع تدفقًا بطريقة تراعي مراحل النشاط باستخدام إدخال سجلّ الرجوع:
قبل:
// In your destination screen or host
val lifecycleOwner = navController.currentBackStackEntry
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
بعد:
// Inside the destination composable
val state by flow.collectAsStateWithLifecycle()
الخطوة 5: نقل وجهاتك من NavGraph في NavHost إلى 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إلى المَعلمةsceneStrategiesفيNavDisplay.
على سبيل المثال:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)
الخطوة 7: إزالة التبعيات في Navigation 2
أزِل جميع عمليات استيراد Navigation 2 والموارد التابعة للمكتبة.
ملخّص
تهانينا! تم الآن نقل مشروعك إلى Navigation 3. إذا واجهت أنت أو وكيل الذكاء الاصطناعي أي مشاكل أثناء استخدام هذا الدليل، يُرجى الإبلاغ عن خطأ هنا.