BottomNavigation (NavigationBar στο Material3) είναι ένα στοιχείο Material Design που παρέχει πλοήγηση μεταξύ τριών έως πέντε κύριων οθονών της εφαρμογής. Στο Jetpack Compose, το κάτω πλαίσιο υλοποιείται μέσω NavigationBar ή BottomNavigation, όπου κάθε στοιχείο αντιπροσωπεύεται από ένα εικονίδιο και μια ετικέτα κειμένου. Σύμφωνα με το Google Material Design (2025), το BottomNavigation συνιστάται για iOS και Android ως το κύριο μοτίβο πρωτογενούς πλοήγησης. Το πλαίσιο προσαρμόζεται αυτόματα στο ύψος της πλοήγησης συστήματος (Gesture Navigation) μέσω WindowInsets. Το NavigationBar στο Material3 υποστηρίζει dynamic color, badge και προσαρμοστικά περιθώρια.
Κύρια σημεία
BottomNavigation είναι ένα στοιχείο Material Design για το κάτω πλαίσιο πλοήγησης που εμφανίζει τρία έως πέντε στοιχεία μενού. Στο Jetpack Compose υλοποιείται μέσω των composable συναρτήσεων BottomNavigation (Material2) και NavigationBar (Material3). Το πλαίσιο τοποθετείται στο κάτω μέρος της οθόνης και παρέχει γρήγορη πρόσβαση σε βασικές ενότητες της εφαρμογής.
Το κύριο καθήκον του BottomNavigation είναι η οργάνωση της πρωτογενούς πλοήγησης μεταξύ επιπέδων οθόνης. Κάθε στοιχείο του πλαισίου αντιπροσωπεύει μια ξεχωριστή οθόνη (Home, Search, Profile). Το πάτημα αλλάζει το περιεχόμενο στην κύρια περιοχή της οθόνης. Το BottomNavigation δεν προορίζεται για ένθετη πλοήγηση — γι' αυτό χρησιμοποιούνται καρτέλες (Tabs) στο επάνω μέρος.
Σύμφωνα με το Material Design Guidelines (2025), το κάτω πλαίσιο πρέπει να περιέχει μόνο εκείνες τις ενότητες που είναι προσβάσιμες από οποιοδήποτε σημείο της εφαρμογής. Η απόκρυψη του πλαισίου σε θυγατρικές οθόνες (π.χ. κατά το άνοιγμα μιας σελίδας λεπτομερειών) είναι συνήθης πρακτική. Στο Compose, η ορατότητα του BottomNavigation διαχειρίζεται μέσω της κατάστασης Scaffold ή υπό όρους απόδοση βάσει της τρέχουσας διαδρομής NavController.
Στο Material3 συνιστάται η χρήση του NavigationBar αντί του Legacy BottomNavigation. Το NavigationBar περιλαμβάνει Dynamic Color, υποστήριξη WindowInsets και βελτιωμένη κινούμενη εικόνα πατήματος. Σύμφωνα με την Google, το NavigationBar είναι ένα μελλοντικό στοιχείο, ενώ το BottomNavigation από M2 παραμένει μόνο για συμβατότητα προς τα πίσω.
Κάθε στοιχείο του NavigationBar υλοποιείται μέσω του composable NavigationBarItem. Παράμετροι στοιχείου: icon (εικονίδιο), label (ετικέτα κειμένου), selected (κατάσταση επιλογής) και onClick (callback). Το 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 πρέπει να είναι Filled για επιλεγμένη κατάσταση και Outlined για μη επιλεγμένη. Το Material Icons παρέχει και τα δύο σύνολα: Icons.Filled.Home και Icons.Outlined.Home. Από προεπιλογή, το NavigationBarItem εμφανίζει την παραλλαγή filled για selected. Εάν χρειάζεται διαφορετική εμφάνιση, δώστε το εικονίδιο υπό όρους: 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 | Callback στο πάτημα |
| 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: Dynamic Color, προσαρμοστικό ύψος, κινούμενη εικόνα ripple σε όλο το πλάτος του στοιχείου και υποστήριξη WindowInsets για τη λήψη υπόψη της πλοήγησης συστήματος.
Το Dynamic Color επιλέγει αυτόματα τα χρώματα του NavigationBar βάσει της ταπετσαρίας της συσκευής (Android 12+). Το ενεργό στοιχείο χρωματίζεται primary, τα ανενεργά — onSurfaceVariant. Από προεπιλογή, το NavigationBar ανυψώνεται πάνω από την πλοήγηση συστήματος μέσω Modifier.navigationBarsPadding(). Για χειροκίνητο έλεγχο, χρησιμοποιήστε την παράμετρο windowInsets.
Το NavigationBar στο M3 κινεί αυτόματα το πλάτος του ενεργού στοιχείου — το εικονίδιο μετακινείται αριστερά και η ετικέτα επεκτείνεται. Η κινούμενη εικόνα διαρκεί 300 ms και χρησιμοποιεί το τυπικό easing του Compose. Εάν αυτή η συμπεριφορά είναι ανεπιθύμητη, ορίστε την ετικέτα πάντα ορατή μέσω alwaysShowLabel = true στο NavigationBarItem.
Για να παρακάμψετε τα χρώματα, δώστε NavigationBarColors στην παράμετρο colors. Από προεπιλογή χρησιμοποιείται NavigationBarDefaults.colors(), αλλά μπορείτε να ορίσετε τα δικά σας χρώματα: containerColor (φόντο πλαισίου), contentColor (χρώμα εικονιδίων), indicatorColor (φόντο ενεργού στοιχείου). Σε αντίθεση με το M2, όπου τα χρώματα ορίζονταν μέσω BottomNavigationDefaults, στο M3 χρησιμοποιείται η ενοποιημένη διεπαφή NavColors.
Εάν στο έργο χρησιμοποιείται BottomNavigation από M2 (androidx.compose.material), η Google συνιστά μετεγκατάσταση σε M3 NavigationBar. Η μετεγκατάσταση περιλαμβάνει αντικατάσταση των import, ενημέρωση παραμέτρων και προσθήκη @OptIn(ExperimentalMaterial3Api::class). Το BottomNavigation από M2 θεωρείται παρωχημένο και δεν λαμβάνει νέες λειτουργίες.
Η κατάσταση NavigationBar αποθηκεύει το ευρετήριο του τρέχοντος επιλεγμένου στοιχείου. Σε απλές περιπτώσεις χρησιμοποιείται remember { mutableIntStateOf(0) }. Ωστόσο, για εφαρμογές παραγωγής, το ευρετήριο πρέπει να συγχρονίζεται με το γράφο πλοήγησης, όχι να είναι τοπική κατάσταση UI.
Οι κύριες προσεγγίσεις διαχείρισης κατάστασης: NavController-based (συνιστάται) και ViewModel-based. Στην πρώτη περίπτωση, το selectedItem υπολογίζεται από την τρέχουσα διαδρομή του NavController. Στη δεύτερη — το ViewModel αποθηκεύει την τρέχουσα οθόνη και το NavigationBarItem διαβάζει το StateFlow από το ViewModel. Το NavController-based είναι απλούστερο, το ViewModel-based παρέχει περισσότερο έλεγχο σε σύνθετη λογική (εξουσιοδότηση, δοκιμές A/B).
Κατά τη χρήση των saveState και restoreState, το NavController αποθηκεύει και επαναφέρει αυτόματα την κατάσταση του LazyColumn και άλλων στοιχείων εντός κάθε καρτέλας. Αυτό εξαλείφει την ανάγκη για ViewModel για αποθήκευση της θέσης κύλισης. Ωστόσο, η αποθήκευση δεν ισχύει για πεδία εισαγωγής — απαιτούν SavedStateHandle ή ViewModel.
Badge είναι ένας συμπαγής δείκτης ειδοποίησης που εμφανίζεται στο εικονίδιο του NavigationBarItem. Στο Material3, το σήμα υλοποιείται μέσω BadgedBox, το οποίο περιβάλλει το εικονίδιο και τοποθετεί το σήμα στην επάνω δεξιά γωνία. Το Badge μπορεί να περιέχει αριθμό (ποσότητα ειδοποιήσεων) ή κουκκίδα (απλή ένδειξη παρουσίας).
@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 έχει από προεπιλογή χρώμα error (κόκκινο) με λευκό κείμενο. Προσαρμογή μέσω BadgeDefaults.colors(containerColor, contentColor).
Στο Material2, το σήμα δεν υπήρχε — έπρεπε να σχεδιαστεί χειροκίνητα μέσω Canvas ή να χρησιμοποιηθούν βιβλιοθήκες τρίτων. Το Material3 έλυσε αυτό το πρόβλημα με ένα εγγενές στοιχείο. Σύμφωνα με το Android Developers (2025), το BadgedBox χρησιμοποιείται στο 40% των εφαρμογών με NavigationBar, εκ των οποίων το 60% για εμφάνιση μη αναγνωσμένων μηνυμάτων.
Συχνές Ερωτήσεις
Το Material Design συνιστά 3 έως 5 στοιχεία. Λιγότερα από τρία — μη σκόπιμο (καλύτερα να χρησιμοποιήσετε Tabs). Περισσότερα από πέντε — υποβαθμίζει την αναγνωσιμότητα των ετικετών και μειώνει το touch-target. Εάν απαιτούνται περισσότερες από 5 ενότητες, χρησιμοποιήστε Navigation Rail (tablet) ή Drawer.
Καθορίστε τη λίστα διαδρομών όπου το πλαίσιο είναι ορατό (συνήθως ρίζες διαδρομές). Στο Scaffold bottomBar, δώστε NavigationBar μόνο εάν το currentRoute βρίσκεται στα bottomNavRoutes. Εναλλακτική — NavHost με ξεχωριστό γράφο για ένθετες οθόνες χωρίς bottomBar.
Το NavigationBar από το Material3 υποστηρίζει Dynamic Color, 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, η τυπική κινούμενη εικόνα είναι fading (fadeIn + fadeOut) με διάρκεια 300ms.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης