BottomNavigation (NavigationBar في Material3) هو مكون من Material Design يوفر التنقل بين ثلاث إلى خمس شاشات رئيسية للتطبيق. في Jetpack Compose، يتم تنفيذ الشريط السفلي عبر NavigationBar أو BottomNavigation، حيث يتم تمثيل كل عنصر بأيقونة وتسمية نصية. وفقًا لـ Google Material Design (2025)، يُوصى باستخدام BottomNavigation لكل من iOS وAndroid كنمط أساسي للتنقل الأولي. يضبط اللوحة تلقائيًا ارتفاع التنقل النظامي (Gesture Navigation) عبر WindowInsets. يدعم NavigationBar في Material3 اللون الديناميكي والشارة والهوامش التكيفية.
الخلاصة
BottomNavigation هو مكون من Material Design لشريط تنقل سفلي يعرض من ثلاثة إلى خمسة عناصر قائمة. في Jetpack Compose، يتم تنفيذه عبر دالتي Composable: BottomNavigation (Material2) وNavigationBar (Material3). يتم وضع الشريط في الجزء السفلي من الشاشة ويوفر وصولاً سريعاً إلى الأقسام الرئيسية للتطبيق.
الغرض الرئيسي من BottomNavigation هو تنظيم التنقل الأولي بين وجهات مستوى الشاشة. كل عنصر في اللوحة يمثل شاشة منفصلة (الرئيسية، البحث، الملف الشخصي). يؤدي النقر إلى تبديل المحتوى في المنطقة الرئيسية للشاشة. BottomNavigation غير مخصص للتنقل المتداخل — لذلك تُستخدم علامات التبويب في الجزء العلوي.
وفقًا لإرشادات Material Design (2025)، يجب أن يحتوي الشريط السفلي فقط على الأقسام التي يمكن الوصول إليها من أي نقطة في التطبيق. إخفاء الشريط في الشاشات الفرعية (مثل فتح صفحة التفاصيل) هو ممارسة قياسية. في Compose، يتم التحكم في رؤية BottomNavigation عبر حالة Scaffold أو العرض الشرطي بناءً على المسار الحالي لـ NavController.
في Material3، يُوصى باستخدام NavigationBar بدلاً من BottomNavigation القديم. يتضمن NavigationBar اللون الديناميكي ودعم WindowInsets وتحسين حركة الضغط. وفقًا لـ Google، NavigationBar هو المكون المستقبلي، بينما يبقى BottomNavigation من M2 فقط للتوافق مع الإصدارات السابقة.
يتم تنفيذ كل عنصر من NavigationBar عبر Composable NavigationBarItem. المعاملات: icon (أيقونة)، label (تسمية نصية)، selected (حالة التحديد)، وonClick (استدعاء). يطبق NavigationBarItem تلقائيًا اللون المحدد (primary) ويحرك تغيير حجم الأيقونة عند التنشيط.
@Composable
fun BottomNavBar() {
val items = listOf(
Screen.Home,
Screen.Search,
Screen.Profile
)
var selectedItem by remember { mutableIntStateOf(0) }
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
يجب أن تكون أيقونات NavigationBarItem مملوءة للحالة المحددة ومحددة للحالة غير المحددة. توفر Material Icons كلتا المجموعتين: Icons.Filled.Home وIcons.Outlined.Home. افتراضيًا، يعرض NavigationBarItem المتغير المملوء للحالة المحددة. إذا كانت هناك حاجة لعرض مختلف، قم بتمرير الأيقونة شرطيًا: if (selected) Filled.X else Outlined.X.
يجب ألا يتجاوز طول التسمية النصية 12 حرفًا (لاتيني) أو 8 أحرف (سيريلي). يتم اقتطاع التسميات الطويلة بعلامات حذف. للتسميات، استخدم maxLines = 1 وellipsis = TextTruncation.Ellipsis. إذا كان يجب عرض التسمية دائمًا، قم بإزالة المعامل alwaysShowLabel = true (القيمة الافتراضية هي true).
| المعامل | النوع | الوصف |
|---|---|---|
| icon | @Composable () -> Unit | أيقونة عنصر اللوحة |
| label | @Composable (() -> Unit)? | تسمية نصية أسفل الأيقونة |
| selected | Boolean | علامة العنصر المحدد |
| onClick | () -> Unit | استدعاء عند النقر |
| badge | @Composable (() -> Unit)? | شارة بعدد الإشعارات |
تحديث selectedItem مباشرة لا يؤدي إلى التنقل — لتبديل الشاشات يلزم التكامل مع NavController. النمط النموذجي: onClick في NavigationBarItem يستدعي navController.navigate(route)، ويتم تحديد selectedItem من المسار الحالي لـ NavController.
يخزن NavController مكدس الرجوع الحالي. لتحديد عنصر اللوحة المحدد، قارن المسار الحالي (navController.currentBackStackEntryAsState().value?.destination?.route) بمسارات العناصر. إذا تطابق المسار، يعتبر العنصر محددًا. هذا يلغي ازدواجية الحالة.
@Composable
fun MainScreen(navController: NavController) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
items.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { /* content */ }
}
الإعدادات الرئيسية في navigate(): popUpTo(startDestination) يمنع تراكم مكدس الرجوع عند تبديل علامات التبويب، launchSingleTop يمنع ازدواجية المسار في المكدس، وrestoreState يستعيد حالة علامة التبويب التي تمت زيارتها سابقًا. بدون هذه المعاملات، ينشئ كل نقرة إدخالاً جديدًا في المكدس، مما يؤدي إلى سلوك غير صحيح لزر الرجوع.
يوفر Material3 (M3) NavigationBar وNavigationBarItem كبديل لـ BottomNavigation من Material2. المزايا الرئيسية لـ M3: اللون الديناميكي، الارتفاع التكيفي، حركة ripple بعرض كامل، ودعم WindowInsets للتنقل النظامي.
يختار اللون الديناميكي تلقائيًا ألوان NavigationBar بناءً على خلفية الجهاز (Android 12+). العنصر النشط يُلون باللون primary، والعناصر غير النشطة بـ onSurfaceVariant. افتراضيًا، يرتفع NavigationBar فوق التنقل النظامي عبر Modifier.navigationBarsPadding(). للتحكم اليدوي، استخدم المعامل windowInsets.
يقوم NavigationBar في M3 تلقائيًا بتحريك عرض العنصر النشط — تنتقل الأيقونة إلى اليسار وتتمدد التسمية. تستمر الحركة 300 مللي ثانية وتستخدم التخفيف القياسي لـ Compose. إذا كان هذا السلوك غير مرغوب فيه، اعرض التسمية دائمًا عبر alwaysShowLabel = true في NavigationBarItem.
لتجاوز الألوان، قم بتمرير NavigationBarColors في المعامل colors. افتراضيًا، يُستخدم NavigationBarDefaults.colors()، ولكن يمكن تعيين قيم مخصصة: containerColor (خلفية اللوحة)، contentColor (لون الأيقونات)، indicatorColor (خلفية العنصر النشط). على عكس M2 حيث تم تعيين الألوان عبر BottomNavigationDefaults، يستخدم M3 واجهة موحدة NavColors.
إذا كان المشروع يستخدم BottomNavigation من M2 (androidx.compose.material)، توصي Google بالترحيل إلى NavigationBar من M3. يتضمن الترحيل استبدال الاستيرادات وتحديث المعاملات وإضافة @OptIn(ExperimentalMaterial3Api::class). يُعتبر BottomNavigation من M2 قديمًا ولا يتلقى ميزات جديدة.
حالة NavigationBar تخزن فهرس العنصر المحدد حاليًا. في الحالات البسيطة، يُستخدم remember { mutableIntStateOf(0) }. ومع ذلك، لتطبيقات الإنتاج، يجب مزامنة الفهرس مع الرسم البياني للتنقل، وليس أن يكون حالة محلية لواجهة المستخدم.
النهج الرئيسية لإدارة الحالة: القائمة على NavController (موصى به) والقائمة على ViewModel. في الحالة الأولى، يتم حساب selectedItem من المسار الحالي لـ NavController. في الثانية، يخزن ViewModel الشاشة الحالية، ويقرأ NavigationBarItem StateFlow من ViewModel. النهج القائم على NavController أبسط، والنظام القائم على ViewModel يوفر تحكمًا أكبر للمنطق المعقد (المصادقة، اختبارات A/B).
عند استخدام saveState وrestoreState، يقوم NavController تلقائيًا بحفظ واستعادة حالة LazyColumn والمكونات الأخرى داخل كل علامة تبويب. هذا يلغي الحاجة إلى ViewModel لتخزين موضع التمرير. ومع ذلك، لا ينطبق الحفظ على حقول الإدخال — فهي تتطلب SavedStateHandle أو ViewModel.
Badge هو مؤشر إشعارات مضغوط يُعرض على أيقونة NavigationBarItem. في Material3، يتم تنفيذ الشارة عبر BadgedBox، الذي يلف الأيقونة ويضع الشارة في الزاوية العلوية اليمنى. يمكن أن تحتوي الشارة على رقم (عدد الإشعارات) أو نقطة (مؤشر بسيط على الوجود).
@Composable
fun HomeItem(unreadCount: Int) {
NavigationBarItem(
icon = {
BadgedBox(badge = {
if (unreadCount > 0) {
Badge { Text(unreadCount.toString()) }
}
}) {
Icon(Icons.Filled.Home, contentDescription = "الرئيسية")
}
},
label = { Text("Home") },
selected = false,
onClick = { }
)
}
يقوم BadgedBox تلقائيًا بتحديد موضع Badge في الزاوية العلوية اليمنى للعنصر الفرعي. إذا تجاوز العدد 99، تعرض Badge "99+". لإخفاء الشارة عندما تكون القيمة صفرًا، استخدم if (count > 0). Badge من Material3 لها افتراضيًا لون خطأ (أحمر) مع نص أبيض. التخصيص عبر BadgeDefaults.colors(containerColor, contentColor).
في Material2، لم تكن الشارة موجودة — كان يجب رسمها يدويًا عبر Canvas أو باستخدام مكتبات خارجية. حل Material3 هذه المشكلة بمكون أصلي. وفقًا لـ Android Developers (2025)، يُستخدم BadgedBox في 40% من التطبيقات التي تحتوي على NavigationBar، منها 60% لعرض الرسائل غير المقروءة.
الأسئلة الشائعة
يوصي Material Design بـ 3 إلى 5 عناصر. أقل من ثلاثة غير عملي (استخدم Tabs بدلاً من ذلك). أكثر من خمسة يقلل من readability التسميات وحجم هدف اللمس. إذا كانت هناك حاجة لأكثر من 5 أقسام، استخدم Navigation Rail (الأجهزة اللوحية) أو Drawer.
حدد قائمة المسارات التي يكون فيها الشريط مرئيًا (عادةً المسارات الجذرية). في Scaffold bottomBar، مرر NavigationBar فقط إذا كان currentRoute في bottomNavRoutes. بديل هو NavHost برسم بياني منفصل للشاشات المتداخلة بدون bottomBar.
NavigationBar من Material3 يدعم اللون الديناميكي وBadgedBox وWindowInsets وعرض العنصر النشط المتحرك. BottomNavigation من Material2 هو API قديم بدون هذه الميزات. توصي Google باستخدام NavigationBar للمشاريع الجديدة وترحيل المشاريع القائمة.
المشكلة هي عدم وجود saveState وrestoreState في navigate(). بدونها، لا يحفظ NavController حالة علامة التبويب السابقة. أضف popUpTo(startDestination) { saveState = true } وrestoreState = true في NavOptions للحفاظ على LazyColumn وموضع التمرير وحقول الإدخال.
يتم التحكم في حركة التبديل على مستوى NavHost، وليس BottomNavigation. أضف composable(route, enterTransition, exitTransition) بحركات مخصصة. بالنسبة لـ BottomNavigation، الحركة النموذجية هي الانقراض (fadeIn + fadeOut) بمدة 300 مللي ثانية.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا