يوفّر مكوِّن التنقّل إمكانية استخدام تطبيقات Jetpack Compose. يمكنك التنقّل بين العناصر القابلة للإنشاء مع الاستفادة من البنية الأساسية والميزات الخاصة بمكوّن Navigation.
للاطّلاع على أحدث إصدار من مكتبة التنقّل في مرحلة الإصدار الأوّلي والمصمَّمة خصيصًا لـ Compose، يُرجى الرجوع إلى مستندات Navigation 3.
ضبط إعدادات الميزة
لاستخدام Compose، أضِف التبعية التالية إلى ملف build.gradle
في وحدة تطبيقك:
Groovy
dependencies { def nav_version = "2.9.1" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.9.1" implementation("androidx.navigation:navigation-compose:$nav_version") }
البدء
عند تنفيذ عملية التنقّل في تطبيق، يجب تنفيذ مضيف تنقّل ورسم بياني ووحدة تحكّم في التنقّل. لمزيد من المعلومات، يُرجى الاطّلاع على النظرة العامة حول التنقّل.
إنشاء NavController
للحصول على معلومات حول كيفية إنشاء NavController
في Compose، راجِع قسم Compose في مقالة إنشاء أداة تحكّم في التنقّل.
إنشاء NavHost
للحصول على معلومات حول كيفية إنشاء NavHost
في Compose، راجِع قسم Compose في مقالة تصميم الرسم البياني للتنقّل.
الانتقال إلى عنصر قابل للإنشاء
للحصول على معلومات حول الانتقال إلى عنصر Composable، راجِع مقالة الانتقال إلى وجهة في مستندات التصميم.
التنقّل باستخدام الوسيطات
للحصول على معلومات حول تمرير وسيطات بين الوجهات القابلة للإنشاء، راجِع قسم Compose في مقالة تصميم الرسم البياني للتنقّل.
استرداد بيانات معقّدة أثناء التنقّل
ننصح بشدة بعدم تمرير عناصر بيانات معقّدة عند التنقّل، بل تمرير الحد الأدنى من المعلومات الضرورية، مثل معرّف فريد أو شكل آخر من أشكال المعرّف، كمعلمات عند تنفيذ إجراءات التنقّل:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
يجب تخزين العناصر المعقّدة كبيانات في مصدر واحد للحقيقة، مثل طبقة البيانات. بعد الوصول إلى وجهتك بعد التنقّل، يمكنك تحميل المعلومات المطلوبة من مصدر المعلومات الموثوق به باستخدام رقم التعريف الذي تم تمريره. لاسترداد الوسيطات في ViewModel
المسؤول عن الوصول إلى طبقة البيانات، استخدِم SavedStateHandle
الخاص بـ ViewModel
:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// …
}
يساعد هذا الأسلوب في منع فقدان البيانات أثناء تغييرات الإعدادات وأي تناقضات عند تعديل العنصر المعنيّ أو تغييره.
للحصول على شرح أكثر تفصيلاً حول سبب ضرورة تجنُّب تمرير بيانات معقّدة كمعلَمات، بالإضافة إلى قائمة بأنواع المَعلمات المتوافقة، يُرجى الاطّلاع على تمرير البيانات بين الوجهات.
روابط لصفحات معيّنة
تتيح مكتبة Navigation Compose الروابط لصفحات في التطبيق التي يمكن تحديدها كجزء من الدالة composable()
أيضًا. تقبل المَعلمة deepLinks
قائمة بعناصر
NavDeepLink
يمكن إنشاؤها بسرعة باستخدام الطريقة
navDeepLink()
:
@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "$uri/profile")
)
) { backStackEntry ->
ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}
تتيح لك هذه الروابط لصفحات معيّنة ربط عنوان URL أو إجراء أو نوع MIME محدّد بأحد العناصر القابلة للإنشاء. وبشكلٍ تلقائي، لا يتم عرض هذه الروابط لصفحات معيّنة للتطبيقات الخارجية. لإتاحة هذه الروابط لصفحات في التطبيق خارجيًا، يجب إضافة عناصر <intent-filter>
المناسبة إلى ملف manifest.xml
الخاص بتطبيقك. لتفعيل الرابط لصفحة معيّنة في المثال السابق، عليك إضافة ما يلي داخل العنصر <activity>
في البيان:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
تنشئ ميزة التنقّل تلقائيًا روابط لصفحات في التطبيق تؤدي إلى هذا العنصر القابل للإنشاء عند تفعيل الرابط من خلال تطبيق آخر.
يمكن أيضًا استخدام الروابط نفسها لصفحات في التطبيق لإنشاء PendingIntent
باستخدام الرابط المناسب لصفحة في التطبيق من عنصر قابل للإنشاء:
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/profile/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
يمكنك بعد ذلك استخدام deepLinkPendingIntent
مثل أي PendingIntent
آخر لفتح تطبيقك في وجهة الرابط لصفحة في التطبيق.
التنقّل المتداخل
للحصول على معلومات حول كيفية إنشاء رسومات بيانية متداخلة للتنقّل، راجِع الرسومات البيانية المتداخلة.
التكامل مع شريط التنقّل السفلي
من خلال تحديد NavController
على مستوى أعلى في التسلسل الهرمي القابل للإنشاء، يمكنك ربط Navigation بمكوّنات أخرى، مثل مكوّن شريط التنقّل السفلي. يتيح لك ذلك التنقّل من خلال النقر على الرموز في الشريط السفلي.
لاستخدام المكوّنين BottomNavigation
وBottomNavigationItem
،
أضِف التبعية androidx.compose.material
إلى تطبيق Android.
رائع
dependencies { implementation "androidx.compose.material:material:1.8.3" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.8.3") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
لربط العناصر في شريط التنقّل السفلي بالمسارات في الرسم البياني للتنقّل، يُنصح بتحديد فئة، مثل TopLevelRoute
الموضّحة هنا، تحتوي على فئة مسار ورمز.
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
بعد ذلك، ضَع هذه المسارات في قائمة يمكن استخدامها من خلال
BottomNavigationItem
:
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
في عنصر BottomNavigation
القابل للإنشاء، احصل على NavBackStackEntry
الحالي باستخدام الدالة currentBackStackEntryAsState()
. يمنحك هذا الإدخال إذن الوصول إلى NavDestination
الحالي. يمكن بعد ذلك تحديد الحالة المحدّدة لكل BottomNavigationItem
من خلال مقارنة مسار العنصر بمسار الوجهة الحالية ووجهاتها الرئيسية للتعامل مع الحالات عند استخدام التنقّل المتداخل باستخدام التسلسل الهرمي NavDestination
.
يُستخدَم مسار العنصر أيضًا لربط دالة onClick
lambda بطلب إلى navigate
، وذلك كي يؤدي النقر على العنصر إلى الانتقال إليه. باستخدام العلامتَين saveState
وrestoreState
، يتم حفظ حالة العنصر وسجلّ الرجوع الخاص به واستعادتهما بشكل صحيح عند التبديل بين عناصر شريط التنقّل السفلي.
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
topLevelRoutes.forEach { topLevelRoute ->
BottomNavigationItem(
icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
label = { Text(topLevelRoute.name) },
selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
onClick = {
navController.navigate(topLevelRoute.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
في هذا المثال، يمكنك الاستفادة من طريقة NavController.currentBackStackEntryAsState()
لرفع حالة navController
خارج الدالة NavHost
ومشاركتها مع المكوّن BottomNavigation
. وهذا يعني أنّ BottomNavigation
يتضمّن تلقائيًا أحدث حالة.
إمكانية التشغيل التفاعلي
إذا أردت استخدام مكوّن Navigation مع Compose، يتوفّر لك خياران:
- تحديد رسم بياني للتنقّل باستخدام "مكوّن التنقّل" من أجل الأجزاء
- حدِّد رسمًا بيانيًا للتنقّل باستخدام
NavHost
في Compose باستخدام وجهات Compose. لا يمكن إجراء ذلك إلا إذا كانت جميع الشاشات في الرسم البياني للتنقّل قابلة للإنشاء.
لذلك، ننصح باستخدام مكوّن التنقّل المستند إلى Fragment في التطبيقات التي تستخدم مزيجًا من Compose وViews. ستحتوي الأجزاء بعد ذلك على شاشات تستند إلى View وشاشات Compose وشاشات تستخدم كلاً من View وCompose. بعد نقل محتوى كل Fragment إلى Compose، تتمثل الخطوة التالية في ربط كل هذه الشاشات معًا باستخدام Navigation Compose وإزالة جميع Fragments.
التنقّل من Compose باستخدام Navigation for fragments
لتغيير الوجهات داخل رمز Compose، عليك عرض الأحداث التي يمكن تمريرها إلى أي عنصر قابل للإنشاء في التسلسل الهرمي وتشغيله:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
في الجزء، يمكنك إنشاء رابط بين Compose ومكوّن Navigation المستند إلى الأجزاء من خلال العثور على NavController
والانتقال إلى الوجهة:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
بدلاً من ذلك، يمكنك تمرير NavController
إلى أسفل تسلسل Compose الهرمي.
ومع ذلك، فإنّ عرض وظائف بسيطة يتيح إعادة استخدامها واختبارها بشكل أكبر.
الاختبار
افصل رمز التنقّل عن وجهاتك القابلة للإنشاء لتتمكّن من اختبار كل عنصر قابل للإنشاء بشكل منفصل عن العنصر القابل للإنشاء NavHost
.
وهذا يعني أنّه يجب عدم تمرير navController
مباشرةً إلى أي عنصر قابل للإنشاء، بل تمرير عمليات معاودة الاتصال الخاصة بالتنقّل كمعلَمات. يتيح ذلك إمكانية اختبار جميع العناصر القابلة للإنشاء بشكل فردي، لأنّها لا تتطلّب مثيلاً من navController
في الاختبارات.
إنّ مستوى التوجيه غير المباشر الذي توفّره دالة composable
lambda هو ما يتيح لك فصل رمز التنقّل عن العنصر القابل للإنشاء نفسه. يمكنك إجراء ذلك في اتجاهَين:
- تمرير الوسيطات التي تم تحليلها فقط إلى العنصر القابل للإنشاء
- مرِّر دوال lambda التي يجب أن يتم تشغيلها بواسطة العنصر القابل للإنشاء من أجل التنقّل، بدلاً من
NavController
نفسه.
على سبيل المثال، قد يكون توقيع ProfileScreen
القابل للإنشاء الذي يتلقّى userId
كإدخال
ويتيح للمستخدمين الانتقال إلى صفحة الملف الشخصي لأحد الأصدقاء على النحو التالي:
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
بهذه الطريقة، يعمل العنصر ProfileScreen
القابل للإنشاء بشكل مستقل عن Navigation، ما يتيح اختباره بشكل مستقل. ستتضمّن دالة composable
lambda الحد الأدنى من التعليمات البرمجية اللازمة لسدّ الفجوة بين واجهات برمجة التطبيقات الخاصة بميزة "التنقّل" والعنصر القابل للإنشاء:
@Serializable data class Profile(id: String)
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
ننصحك بكتابة اختبارات تغطّي متطلبات التنقّل في تطبيقك من خلال اختبار NavHost
وإجراءات التنقّل التي يتم تمريرها إلى العناصر القابلة للإنشاء، بالإضافة إلى العناصر القابلة للإنشاء الخاصة بالشاشة الفردية.
اختبار NavHost
لبدء اختبار NavHost
، أضِف تبعية اختبار التنقّل التالية:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
غلِّف NavHost
لتطبيقك بعنصر قابل للإنشاء يقبل NavHostController
كمعلَمة.
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
يمكنك الآن اختبار AppNavHost
وجميع منطق التنقّل المحدّد داخل NavHost
من خلال تمرير مثيل من عنصر اختبار التنقّل TestNavHostController
. سيبدو اختبار واجهة المستخدم الذي يتحقّق من وجهة البدء في تطبيقك وNavHost
على النحو التالي:
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Before
fun setupAppNavHost() {
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
AppNavHost(navController = navController)
}
}
// Unit test
@Test
fun appNavHost_verifyStartDestination() {
composeTestRule
.onNodeWithContentDescription("Start Screen")
.assertIsDisplayed()
}
}
اختبار إجراءات التنقّل
يمكنك اختبار تنفيذ التنقّل بعدة طرق، وذلك من خلال النقر على عناصر واجهة المستخدم ثم التحقّق من الوجهة المعروضة أو من خلال مقارنة المسار المتوقّع بالمسار الحالي.
بما أنّك تريد اختبار تنفيذ تطبيقك المحدّد، يُفضّل استخدام النقرات على واجهة المستخدم. للتعرّف على كيفية اختبار ذلك إلى جانب الدوال الفردية القابلة للإنشاء بشكل منفصل، احرص على الاطّلاع على الدرس التطبيقي الاختبار في Jetpack Compose.
يمكنك أيضًا استخدام navController
للتحقّق من صحة تأكيداتك من خلال مقارنة المسار الحالي بالمسار المتوقّع باستخدام currentBackStackEntry
في navController
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
للحصول على مزيد من الإرشادات حول أساسيات اختبار Compose، يمكنك الاطّلاع على اختبار تصميم Compose والتدريب العملي حول الاختبار في Jetpack Compose. لمزيد من المعلومات حول الاختبار المتقدّم لرمز التنقّل، يُرجى الانتقال إلى دليل اختبار التنقّل.
مزيد من المعلومات
لمزيد من المعلومات حول Jetpack Navigation، يمكنك الاطّلاع على بدء استخدام مكوّن Navigation أو تجربة التدريب العملي حول التنقّل في Jetpack Compose.
للتعرّف على كيفية تصميم نظام التنقّل في تطبيقك ليتكيّف مع أحجام الشاشات المختلفة واتجاهاتها وأشكالها، اطّلِع على التنقل في واجهات المستخدم المتجاوبة.
للتعرّف على طريقة أكثر تقدّمًا لتنفيذ ميزة التنقّل في Compose في تطبيق مقسَّم إلى وحدات، بما في ذلك مفاهيم مثل الرسوم البيانية المتداخلة ودمج شريط التنقّل السفلي، يمكنك الاطّلاع على تطبيق Now in Android على GitHub.
نماذج
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- Material Design 2 في Compose
- نقل Jetpack Navigation إلى Navigation Compose
- مكان رفع الحالة