Android Coden
Android 7 min lesen

Navigation Bars in Jetpack Compose: Tabs und Bottom Navigation

Erfahre, wie du primäre App-Ziele mit Navigation Bars strukturierst und übersichtlich darstellst.

Eine gut strukturierte App steht und fällt mit ihrer Menüführung. Wenn Nutzer deine Anwendung öffnen, müssen sie sofort und intuitiv erkennen können, wo sie sich befinden und wie sie zu den zentralen Funktionen gelangen. Hier kommen Navigation Bars ins Spiel. Sie sind das visuelle Rückgrat deiner App-Struktur und sorgen dafür, dass die primären Zielorte – die sogenannten Destinations – stets griffbereit bleiben. Egal, ob es sich um eine Bottom Navigation am unteren Bildschirmrand oder um Tabs im oberen Bereich handelt, das architektonische Ziel ist immer identisch: Eine klare, vorhersehbare und barrierefreie Steuerung anzubieten, die den mentalen Aufwand für den Nutzer minimiert und eine logische Orientierung im Raum der Anwendung gewährleistet.

Was ist das?

Navigation Bars sind essenzielle UI-Komponenten, die die wichtigsten Hauptziele einer mobilen Anwendung auf oberster Ebene präsentieren und strukturieren. In der Android-Entwicklung fungieren sie als ständige, verlässliche Begleiter des Nutzers. Sie ermöglichen es, schnell zwischen verschiedenen, fachlich gleichwertigen Kontexten zu wechseln, ohne den aktuellen Navigationsstapel tief durchdringen oder über viele Zwischenschritte zurücknavigieren zu müssen. Stell dir die Architektur deiner App wie einen großen, gut organisierten Aktenschrank vor: Die Navigation Bar stellt die leicht zugänglichen Hauptschubladen dar, während die tiefergehende Navigation innerhalb dieser Bereiche den einzelnen Ordnern und Dokumenten entspricht.

Es gibt verschiedene Ausprägungen dieses UI-Musters, die je nach Anwendungsfall gewählt werden sollten. Die bekannteste Variante ist die klassische Bottom Navigation, die besonders auf modernen, großen Mobiltelefonen weit verbreitet ist. Sie befindet sich ergonomisch günstig in der Nähe des Daumens und bietet dadurch maximalen Bedienkomfort. Design-Richtlinien empfehlen den Einsatz einer Bottom Navigation idealerweise für drei bis maximal fünf Hauptziele. Sobald deine App mehr als fünf zentrale Einstiegspunkte erfordert, wird die Leiste visuell überladen und motorisch schwer bedienbar; in solchen Fällen solltest du über alternative Navigationsmuster nachdenken, beispielsweise einen Navigation Drawer.

Tabs (Reiter) hingegen werden häufig verwendet, um eng verwandte Datenansichten auf exakt derselben Hierarchieebene zu organisieren. Sie eignen sich hervorragend für unterschiedliche Kategorien von Nachrichtenfeeds oder weitreichende Filtermöglichkeiten innerhalb eines spezifischen App-Bereichs. Beide Formen, Bottom Navigation und Tabs, teilen jedoch das gleiche konzeptionelle Ziel: Sie bieten schnellen, verlässlichen Zugriff auf primäre Destinations. Im Kontext der modernen Android-Entwicklung mit Kotlin und Jetpack Compose werden diese Muster durch spezifische, gut durchdachte Composables abgebildet, die sich nahtlos in das Material Design 3 integrieren und von Haus aus Barrierefreiheit wie Screenreader-Unterstützung mitbringen.

Wie funktioniert es?

In der klassischen, View-basierten Android-Welt war die Implementierung von Navigationsstrukturen oft fehleranfällig. Du musstest dich mit komplexen XML-Layouts, statischen Menü-Ressourcen und der manuellen Verwaltung von Fragment-Transaktionen befassen, um eine durchgängig funktionierende Bottom Navigation zu gewährleisten. Mit Jetpack Compose hat sich dieses mentale Modell grundlegend vereinfacht und modernisiert. Die Mechanik basiert nun auf einem streng deklarativen Ansatz, bei dem der Zustand (State) direkt und reaktiv die angezeigte Benutzeroberfläche steuert.

Das architektonische Herzstück der Umsetzung bildet die NavigationBar-Komponente. Sie dient als flexibler Container für mehrere NavigationBarItem-Elemente. Jedes einzelne Item repräsentiert dabei ein spezifisches, eigenständiges Ziel. Der Lebenszyklus der Navigation ist engmaschig an den NavController der Jetpack Navigation Component gekoppelt, welcher den Zustand der aktuellen Route und den gesamten Backstack verwaltet. Wenn der Nutzer auf ein Tab oder ein Icon in der Bottom Navigation tippt, wird ein Navigationsereignis ausgelöst, das den Zustand des Controllers modifiziert. Compose reagiert auf diese Zustandsänderung automatisch und rendert den betroffenen Bildschirmbereich effizient neu, um den entsprechenden Inhalt zur Anzeige zu bringen.

Der entscheidende Unterschied zu herkömmlichen Interaktionselementen wie simplen Buttons besteht darin, dass Navigation Bars einen “ausgewählten” Zustand besitzen, der dem Nutzer visuell deutlich gemacht werden muss. Das bedeutet für dich als Entwickler: Du musst programmgesteuert stets wissen, welche Destination gerade aktiv ist. Dies erreichst du am besten, indem du den aktuellen Backstack-Eintrag aus dem NavController als Compose-State beobachtest. Sobald sich die aktive Route ändert, gleicht die Navigation Bar den neuen Zustand mit ihren iterierten Items ab und markiert das exakt passende Element als selektiert. Dieser reaktive, datengetriebene Fluss verhindert verlässlich Inkonsistenzen zwischen dem, was der Nutzer optisch wahrnimmt, und dem tatsächlichen internen Status der Applikation.

Zusätzlich bietet das Material Design 3 in Compose native Unterstützung für feine, aber wichtige Details wie Badges – kleine visuelle Indikatoren für neue Benachrichtigungen an einem Icon – oder flüssige Animationen beim Wechsel zwischen den Zuständen. Da Jetpack Compose stark auf Architekturprinzipien wie Unidirectional Data Flow und State Hoisting setzt, solltest du den Navigationsstatus so weit wie möglich nach oben in deiner Komponenten-Hierarchie verlagern. Idealerweise existiert die Bottom Navigation direkt in dem Haupt-Scaffold deiner App, wodurch sie vom Zustand der einzelnen Unterbildschirme entkoppelt bleibt.

In der Praxis

Lass uns konkret werden und eine typische, robuste Implementierung einer Bottom Navigation in Jetpack Compose durchspielen. Stell dir vor, wir entwickeln eine Anwendung mit exakt drei Hauptbereichen: Startseite, Suche und Profil. Um Typensicherheit zu gewährleisten und Tippfehler bei Routen-Strings zu vermeiden, definieren wir zuerst ein strukturiertes Datenmodell für unsere Destinations.

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState

sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
    object Home : Screen("home", "Startseite", Icons.Filled.Home)
    object Search : Screen("search", "Suche", Icons.Filled.Search)
    object Profile : Screen("profile", "Profil", Icons.Filled.Person)
}

@Composable
fun AppBottomNavigation(navController: NavController) {
    val items = listOf(Screen.Home, Screen.Search, Screen.Profile)

    NavigationBar {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route

        items.forEach { screen ->
            NavigationBarItem(
                icon = { Icon(screen.icon, contentDescription = screen.title) },
                label = { Text(screen.title) },
                selected = currentRoute == screen.route,
                alwaysShowLabel = true,
                onClick = {
                    navController.navigate(screen.route) {
                        popUpTo(navController.graph.startDestinationId) {
                            saveState = true
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    }
}

In diesem Codeblock siehst du ein entscheidendes Detail, das in der Praxis oft zu gravierenden Stolperfallen führt: die korrekte Konfiguration des onClick-Verhaltens. Eine häufige Fehlerquelle bei Anfängern ist, dass bei jedem Tippen auf ein Tab eine völlig neue Instanz des Zielbildschirms auf den Backstack gelegt wird. Tippt der Nutzer wild zwischen den Reitern hin und her, muss er später dutzende Male die Zurück-Taste drücken, um die App regulär zu verlassen. Das resultiert in einer schlechten Nutzererfahrung und unnötigem Speicherverbrauch.

Um dieses ineffiziente und verwirrende Verhalten zu vermeiden, setzen wir zwingend drei wichtige Flags im Navigationsaufruf:

  1. popUpTo: Wir räumen den Backstack konsequent bis zum definierten Startziel auf, speichern aber explizit den Zustand der verlassenen Route (saveState = true). Das verhindert, dass der Stack unkontrolliert anwächst.
  2. launchSingleTop = true: Dies verhindert effektiv, dass dasselbe Ziel mehrfach hintereinander auf den Stack gelegt wird, wenn der Nutzer beispielsweise hastig doppelt auf dasselbe Icon tippt.
  3. restoreState = true: Kehrt der Nutzer zu einem zuvor besuchten Tab zurück, wird dessen vollständiger Zustand nahtlos wiederhergestellt (zum Beispiel die exakte Scrollposition in einer langen Liste oder eingegebener Text in einem Suchfeld), was die Usability massiv verbessert.

Eine weitere Workflow-Empfehlung aus der Praxis betrifft die Sichtbarkeit der Navigation Bar. Oftmals möchtest du die Leiste auf tieferen Ebenen (wie einer Detailansicht) verbergen. Anstatt die Sichtbarkeit auf Basis der aktuellen Route mühsam über komplexe if-else-Kaskaden im Scaffold zu steuern, ist es oft sauberer, spezifische Detail-Bildschirme in einer separaten Navigationsstruktur oder als Full-Screen-Dialoge zu modellieren, die sich visuell vollständig über das Scaffold mit der Bottom Navigation legen.

Fazit

Navigation Bars und Tabs sind essenzielle Werkzeuge, um die primären Destinations in deiner App-Architektur zugänglich, robust und visuell übersichtlich zu gestalten. Mit Jetpack Compose setzt du diese Muster elegant und deklarativ um, indem du funktionale UI-Komponenten wie die NavigationBar eng an den reaktiven Zustand deines NavController bindest. Um dein Wissen praktisch zu festigen, baue das obige Beispiel in einem Testprojekt nach. Nutze den Layout Inspector des Android Studios sowie den Debugger, um den Zustand des Backstacks beim intensiven Wechseln der Tabs präzise zu beobachten. Teste ganz bewusst, wie sich das System verhält, wenn du die essenziellen Flags launchSingleTop und restoreState testweise entfernst. Ein abschließendes Code-Review deiner Navigationslogik hilft zudem sicherzustellen, dass keine ungewollten Instanzen den Speicher belasten und die Nutzerführung stets logisch sowie vorhersehbar bleibt.

Quellen (1)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.