NavController هو المكون المركزي لمكتبة Navigation Compose، الذي يدير مكدس التنقل وحالة back stack في تطبيقات Android. من خلال NavController تتم الانتقالات بين الشاشات، والعودة إلى الصفحات السابقة، ونقل البيانات بين المسارات. وفقًا لـ Android Developers (2025)، NavController هو عنصر إلزامي لأي تطبيق Compose يحتوي على أكثر من شاشة واحدة. يتم إنشاء المتحكم عبر rememberNavController()، ويمرر إلى NavHost، ويكون متاحًا لاستدعاء navigate() من أي نقطة في التركيبة. الدعم المدمج لـ SavedStateHandle يحفظ تلقائيًا حالة ViewModel أثناء إعادة التهيئة.
النقاط الرئيسية
NavController هو فئة من مكتبة Navigation Compose تنفذ متحكم التنقل لتطبيقات Compose. يدير NavController مكدس NavBackStackEntry، حيث يحتوي كل إدخال على المسار والوسائط وحالة الشاشة. يدعم المتحكم عمليات التنقل الأساسية: الانتقال، العودة، الاستبدال والتنظيف.
على عكس نظام View، حيث كان التنقل يتم عبر FragmentManager أو Intent، يعمل NavController حصريًا في سياق Compose. يتم تخزين back stack كرسم بياني NavDestination بدلاً من مكدس Fragment. هذا يلغي الحمل الزائد لإنشاء وتدمير Fragment ويبسط الاختبار — يمكن محاكاة NavController عبر TestNavHostController.
يرتبط NavController ارتباطًا وثيقًا بـ NavHost — حاوية تعرض الشاشة الحالية من الرسم البياني. بدون NavHost، لا يمكن لـ NavController عرض دوال composable لكنه يحتفظ بالقدرة على إدارة المكدس. في البنية النموذجية، يتم إنشاء NavController على مستوى Activity أو composable الرئيسي ويمرر إلى أسفل شجرة التركيبة عبر المعاملات.
وفقًا لـ Google، مر NavController بعدة إصدارات رئيسية. أضافت النسخة 2.8.0 التنقل الآمن للأنواع (Type-Safe Navigation)، وأضافت النسخة 2.9.0 دعم الإيماءة الخلفية التنبؤية (Android 14+). المتحكم متوافق مع Material3 Scaffold و BottomNavigation. للمشاريع متعددة الوحدات، يمرر NavController عبر DI (Hilt/Koin) أو معاملات المنشئ.
يتم إنشاء NavController عبر الدالة composable rememberNavController(). تعيد الدالة مثيلاً من NavHostController (فئة فرعية من NavController) مرتبطًا بدورة حياة composable الحالي. عند الخروج من التركيبة، يتم تنظيف المتحكم. للحفاظ على المتحكم أثناء إعادة التهيئة، استخدم rememberSaveable أو ViewModel.
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "main"
) {
composable("main") { MainScreen(navController) }
composable("details") { DetailsScreen(navController) }
}
}
يتضمن تكوين NavController: NavHostController (رئيسي)، TestNavHostController (اختبار) و ScopedNavController (فرعي للرسوم البيانية المتداخلة). بالنسبة لـ BottomNavigation، يجب أن يكون NavController واحدًا للتطبيق بأكمله — إنشاء متحكم جديد في كل تبويب سيؤدي إلى فقدان المكدس. لتمرير المتحكم إلى الشاشات المتداخلة، استخدم معامل دالة بدلاً من CompositionLocalProvider للحفاظ على readability.
لاختبار التنقل، استخدم TestNavHostController مع compose-test-rule. يسمح المتحكم بتعيين المسار الأولي والتحقق من أن navigate() أطلق الانتقال المتوقع. اختبار NavController لا يتطلب محاكيًا — يعمل مع matchrs Semantics الخاصة بـ Compose Test.
طريقة navigate(route: String) هي الآلية الأساسية للتنقل في NavController. تقبل سلسلة المسار، و NavOptions اختيارية و Navigator.Extras. تتحكم NavOptions في سلوك الانتقال: launchSingleTop (عدم تكرار المسار في المكدس)، popUpTo (تنظيف المكدس حتى مسار محدد)، restoreState (استعادة الحالة السابقة).
يتم تعيين NavOptions عبر صيغة builder: NavOptionsBuilder. المعاملات الرئيسية: popUpTo (مسار + inclusive/saveState)، launchSingleTop (Boolean، true — عدم إنشاء نسخ مكررة)، restoreState (استعادة الحالة عند العودة). بدون popUpTo، كل navigate() يضيف إدخالاً إلى المكدس، مما يؤدي إلى تراكم back stack وسلوك غير صحيح لزر Back.
navController.navigate("profile/42") {
popUpTo("main") { saveState = true }
launchSingleTop = true
restoreState = true
}
يسمح Navigator.Extras بتمرير بيانات إضافية غير جزء من المسار: عناصر مشتركة للرسوم المتحركة، أعلام Intent، حزمة Pac-Man. نادرًا ما تُستخدم Extras — بشكل أساسي للتكامل مع Accompanist Animation أو Navigators مخصصة. لمعظم السيناريوهات، سلسلة المسار و NavOptions كافية.
popBackStack() هي طريقة العودة إلى الشاشة السابقة. بدون وسائط، تزيل الإدخال العلوي للمكدس وتعيد true إذا كان الإزالة ناجحة. إذا كان المكدس فارغًا، تعيد الطريقة false ويتم إغلاق Activity (مشابه لـ super.onBackPressed()).
النسخة المحملة بشكل زائد popBackStack(route: String, inclusive: Boolean) تزيل جميع الإدخالات حتى المسار المحدد. إذا كان inclusive = true، يتم أيضًا إزالة المسار المحدد نفسه. تعيد الطريقة Boolean — true إذا تم العثور على الإدخالات وإزالتها. النسخة مع inclusive مفيدة لسيناريوهات «الخروج إلى الشاشة الجذرية» بعد التفويض أو إتمام الطلب.
| الطريقة | الوصف | مثال |
|---|---|---|
| popBackStack() | العودة شاشة واحدة للخلف | navController.popBackStack() |
| popBackStack(route, false) | تنظيف حتى route (route يبقى) | popBackStack(“home”, false) |
| popBackStack(route, true) | تنظيف حتى وشامل route | popBackStack(“home”, true) |
| navigate(route) { popUpTo(route) { inclusive = true } } | انتقال مع تنظيف كامل | navigate(“login”) { popUpTo(0) { inclusive = true } } |
لمعالجة زر Back النظامي (hardware back button)، استخدم BackHandler من Compose. يقبل BackHandler enabled و onBack — استدعاء يتم عند الضغط. للإصدار Android 14+، يُستخدم PredictiveBackGesture، المدمج عبر NavController من الإصدار 2.9.0. Predictive back يضيف رسوم متحركة لمعاينة العودة.
SavedStateHandle هي آلية للحفاظ على حالة ViewModel أثناء التنقل وإعادة التهيئة. يوفر NavController تلقائيًا SavedStateHandle لكل NavBackStackEntry. عبر SavedStateHandle، يخزن ViewModel حالة الشاشة ويستعيدها عند العودة (restoreState = true).
في Navigation Compose، يُستخدم SavedStateHandle مع ViewModel: تتم تهيئة ViewModel عبر SavedStateHandle، الذي يمرر من backStackEntry. عند الانتقال إلى شاشة أخرى والعودة (مع restoreState)، يتلقى ViewModel الحالة المحفوظة بدلاً من إنشائه من جديد. هذا ضروري للشاشات ذات إدخال البيانات أو الفلاتر أو التمرير.
class ProfileViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId: String = savedStateHandle.get<String>("userId") ?: ""
var searchQuery by savedStateHandle.getStateFlow("search", "")
.collectAsState()
}
يدعم SavedStateHandle الأنواع البدائية، String، Bundle و Parcelable. للكائنات المعقدة، احفظ المعرفات فقط وحمل البيانات الكاملة من المستودع. حد SavedStateHandle حوالي 1 ميجابايت، تجاوزه يسبب TransactionTooLargeException. للأحجام الكبيرة، استخدم Room أو DataStore بدلاً من الحفظ في handle.
هام: يحفظ SavedStateHandle الحالة فقط عند استخدام restoreState = true في NavOptions. إذا لم يتم تحديد restoreState، يتم إنشاء ViewModel من جديد بقيم افتراضية عند العودة. لتبديل BottomNavigation مع restoreState، يحفظ NavController حالة كل تبويب ويستعيدها عند إعادة التحديد.
currentBackStackEntryAsState() هي دالة تعيد State<NavBackStackEntry?>، والتي تتحدث مع كل تغيير للمسار الحالي. هذه هي الآلية الأساسية لمزامنة واجهة المستخدم مع التنقل: BottomNavigation يبرز العنصر النشط، Toolbar يحدث العنوان، Drawer يغلق عند الانتقال.
تعمل الدالة عبر snapshotFlow و collectAsState: عندما يتغير back stack، يعيد Compose تركيب العناصر المشتركة. هام: currentBackStackEntryAsState() يتم تحديثها فقط بعد اكتمال رسم متحرك للانتقال. للتحديثات الفورية، استخدم currentDestination الذي يتغير بشكل متزامن مع navigate() لكنه لا يدعم الحالة.
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Text(
text = when (currentRoute) {
"home" -> "Home"
"profile" -> "Profile"
else -> ""
}
)
للوصول إلى وسائط المسار الحالي، استخدم navBackStackEntry?.arguments. هذا مناسب في BottomNavigation: يتم حساب selectedItem بناءً على currentRoute. لتصحيح أخطاء التنقل، استخدم NavController.addOnDestinationChangedListener() الذي يسجل كل انتقال. في الإنتاج، تجنب الاشتراكات داخل عدد كبير من composables — أنشئ مصدرًا واحدًا في ViewModel ومرر State إلى واجهة المستخدم.
الأسئلة الشائعة
نعم تقنيًا، لكن لا يُنصح بذلك. متحكم واحد يضمن back stack متناسق ويبسط التصحيح. المتحكمات المتعددة مبررة فقط للرسوم البيانية المتداخلة ذات التنقل المنفصل (مثل modal bottom sheet مع مكدس خاص بها).
مرر NavController إلى ViewModel عبر المنشئ أو DI. لكن من الأفضل تمرير دوال الاستدعاء فقط (onNavigate, onBack) بدلاً من NavController نفسه — هذا يبسط الاختبار. للأحداث، استخدم Channel<NavEvent> في ViewModel واجمع في واجهة المستخدم.
المشكلة في دورة الحياة: إذا لم يتم تهيئة NavController بعد (NavHost لم ينشأ)، يتم تجاهل navigate(). استخدم LaunchedEffect لاستدعاء التنقل بعد تحميل البيانات، وليس داخل coroutine بدورة حياة عشوائية.
استدع navController.navigate(“target”) { popUpTo(0) { inclusive = true } }. المعامل popUpTo(0) ينظف المكدس بالكامل، inclusive = true يزيل أيضًا الإدخال الأولي. العلم launchSingleTop = true يمنع تكرار المسار الجديد.
NavHostController هو فئة فرعية من NavController بطرق إضافية لـ NavHost (مثل setOnBackStackChangedListener). NavController هو الفئة الأساسية التي يمكن استخدامها خارج NavHost لإدارة المكدس برمجيًا. في معظم الحالات، يُستخدم NavHostController.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا