Flutter: BottomNavigationBar im Vergleich zu NavigationBar
Zuletzt aktualisiert:
7 Min. Lesezeit

Flutter bringt zwei Widgets für die untere Navigation mit: BottomNavigationBar aus der Material-2-Ära und NavigationBar, das mit Material 3 hinzukam. Beide erledigen dieselbe Aufgabe, den Wechsel zwischen drei bis fünf Hauptzielen; die Unterschiede liegen im Aussehen, in den API-Namen und in der Art der Anpassung. Dies ist kein „So verwenden Sie es“-Beitrag, sondern eine Entscheidungs- und Migrationshilfe: welches Widget für welches Projekt, was aus jeder Eigenschaft wird, wenn Sie eine bestehende Leiste umziehen, und welche Unterschiede nach dem Wechsel am meisten überraschen. Die Grundlagen, die für beide Widgets gelten, currentIndex im State halten und den Seitenzustand mit IndexedStack bewahren, habe ich im Beitrag zur Verwendung der BottomNavigationBar ausführlich beschrieben und wiederhole sie hier nicht.
Live-Demo
Sie können das Beispiel zur unteren Navigation in der interaktiven Demo unten ausprobieren:
💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.
Die beiden Widgets nebeneinander
Dieselben drei Ziele, zwei verschiedene Widgets:
// Material-2-Generation
BottomNavigationBar(
currentIndex: index,
onTap: (value) => setState(() => index = value),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Start'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suche'),
BottomNavigationBarItem(icon: Icon(Icons.person_outline), activeIcon: Icon(Icons.person), label: 'Profil'),
],
)
// Material-3-Generation
NavigationBar(
selectedIndex: index,
onDestinationSelected: (value) => setState(() => index = value),
destinations: const [
NavigationDestination(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Start'),
NavigationDestination(icon: Icon(Icons.search), label: 'Suche'),
NavigationDestination(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profil'),
],
)Die Struktur ist identisch; nur das Vokabular hat sich geändert. Dass NavigationBar das Wort „Destination“ wählt, ist kein Zufall: Dasselbe Vokabular verwenden NavigationRail und NavigationDrawer, was den Wechsel zwischen den drei Widgets erleichtert. Was das bringt, zeigt das Szenario am Ende.
Tabelle zur Eigenschaftszuordnung
Die Tabelle, die Sie während einer Migration neben sich liegen haben sollten:
| BottomNavigationBar | NavigationBar | Hinweis |
|---|---|---|
items |
destinations |
Anderer Listentyp |
BottomNavigationBarItem |
NavigationDestination |
label ist in beiden ein String |
currentIndex |
selectedIndex |
Lebt weiterhin im State |
onTap |
onDestinationSelected |
Gleiche Signatur: void Function(int) |
activeIcon |
selectedIcon |
Fällt auf icon zurück, wenn nicht gesetzt |
type: fixed / shifting |
keine | NavigationBar arbeitet immer mit gleichen Breiten |
showUnselectedLabels, showSelectedLabels |
labelBehavior |
Ein einziges Enum |
selectedItemColor, unselectedItemColor |
NavigationBarThemeData.iconTheme / labelTextStyle |
Zustandsabhängige Farben |
backgroundColor, elevation |
backgroundColor, elevation |
Gleich |
| keine | indicatorColor, indicatorShape |
Die Pille hinter dem gewählten Element |
BottomNavigationBarItem.backgroundColor |
keine | Hintergrund pro Element gibt es in M3 nicht |
Sichtbare Unterschiede
NavigationBar zeichnet hinter dem gewählten Element einen pillenförmigen Indikator. Seine Farbe stammt aus secondaryContainer des Themes, und bei einem Wechsel gleitet er animiert unter das Icon. BottomNavigationBar hebt nur über Icon- und Beschriftungsfarbe hervor.
Die Höhe unterscheidet sich: NavigationBar ist standardmäßig 80 logische Pixel hoch und zeigt die Beschriftungen immer; die klassische Leiste ist niedriger. Das Gefühl „die Leiste ist dicker geworden“ nach einer Migration kommt von hier. Über den Parameter height lässt sie sich verkleinern, aber entscheiden Sie das bewusst: 80 ist die Empfehlung von Material 3 für Touch-Ziele.
Das Verhalten der Beschriftungen steuert ein einziges Enum:
NavigationBar(
labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, // alwaysShow (Standard), alwaysHide
// ...
)Das Verhalten „nur die gewählte Beschriftung ist sichtbar“ des Typs shifting entspricht onlyShowSelected; die Hintergrundfarbe pro Element hat in Material 3 kein Gegenstück, und Sie sollten auch keines suchen.
Theming mit NavigationBarThemeData
Bei BottomNavigationBar schrieben wir die Farben direkt ans Widget. NavigationBar erwartet stattdessen ein Theme auf Basis von WidgetStateProperty, in dem der gewählte und der nicht gewählte Zustand in einer Funktion aufgelöst werden:
final scheme = ColorScheme.fromSeed(seedColor: Colors.teal);
ThemeData(
colorScheme: scheme,
navigationBarTheme: NavigationBarThemeData(
height: 72,
indicatorColor: scheme.primaryContainer,
labelTextStyle: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
return TextStyle(
fontSize: 12,
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
);
}),
iconTheme: WidgetStateProperty.resolveWith((states) {
final selected = states.contains(WidgetState.selected);
return IconThemeData(
color: selected ? scheme.onPrimaryContainer : scheme.onSurfaceVariant,
);
}),
),
)Das sieht auf den ersten Blick länger aus, aber der Gewinn ist: Jede NavigationBar in der App wird von einer Stelle aus verwaltet, und wenn sich colorScheme für den Dunkelmodus ändert, folgen die Farben automatisch. Das Widget hat auch Parameter wie indicatorColor und backgroundColor; die verwenden Sie, wenn ein einzelner Screen das Theme überschreiben muss.
Welches wann?
- NavigationBar wählen, wenn Sie ein neues Projekt beginnen und
ThemeDatamit Material 3 läuft (in aktuellen Flutter-Versionen der Standard). Themenfarben, Indikator und Typografie kommen fertig, und das Gespräch mit dem Designer über die Material-3-Richtlinien wird einfacher. - NavigationBar wählen, wenn Tablet- oder Web-Unterstützung geplant ist. Weil dieselbe Zielliste auch an eine
NavigationRailübergeben werden kann, ist der Wechsel zu einer seitlichen Leiste auf breiten Bildschirmen eine Sache weniger Zeilen. - Bei BottomNavigationBar bleiben, wenn das Projekt mit
useMaterial3: falseim Material-2-Look läuft und keine Theme-Migration ansteht. Die beiden Widgets zu mischen lässt die App uneinheitlich wirken. - Bei BottomNavigationBar bleiben, wenn die Hintergrundfarbe pro Element (
shifting) Teil des Designs ist. M3 kennt dieses Verhalten nicht. - Keines von beiden verwenden, wenn es mehr als fünf Ziele gibt. Verschieben Sie die übrigen in einen Drawer; das Material-3-Gegenstück ist
NavigationDrawer.
Ist die Wahl getroffen, bleibt der Teil, der in beiden gleich ist: Der gewählte Index lebt im State, und body wird ein IndexedStack, um den Seitenzustand zu bewahren.
Migrationsschritte
Eine bestehende BottomNavigationBar umzuziehen ist meist eine Sache von zehn Minuten:
- Machen Sie aus der Liste
itemseine Listedestinations; wandeln Sie jedesBottomNavigationBarItemin eineNavigationDestinationum und benennen SieactiveIconinselectedIconum. currentIndex→selectedIndex,onTap→onDestinationSelected. Ihr State-Feld und IhresetState-Logik bleiben unverändert.- Löschen Sie die Zeile
type. Wenn SieshowUnselectedLabels: falseverwendet haben, schreiben Sie stattdessenlabelBehavior: onlyShowSelected. - Verschieben Sie
selectedItemColorundunselectedItemColoriniconThemeundlabelTextStyleeinerNavigationBarThemeData. - Starten Sie die App und prüfen Sie die Höhe; setzen Sie bei Bedarf
height.
Häufige Fehler
1. Sie suchen selectedItemColor, und es gibt sie nicht
Symptom: Sie finden an NavigationBar keinen Parameter, um die Farbe des gewählten Icons zu ändern; indicatorColor färbt nur die Pille dahinter.
Ursache: Material 3 vergibt Farben über zustandsabhängig aufgelöste Theme-Eigenschaften.
Lösung: Geben Sie NavigationBarThemeData.iconTheme wie oben gezeigt ein WidgetStateProperty.resolveWith, das für gewählt und nicht gewählt jeweils ein IconThemeData liefert. Derselbe Ansatz gilt für labelTextStyle.
2. Nach der Migration sind alle Beschriftungen sichtbar
Symptom: Die alte Leiste zeigte nur die Beschriftung des gewählten Elements; die neue zeigt alle.
Ursache: Das Standard-labelBehavior von NavigationBar ist alwaysShow; das Ausblendverhalten von shifting wird nicht automatisch übernommen.
Lösung: labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected setzen. Aus Sicht der Barrierefreiheit ist es meist besser, Beschriftungen zu zeigen; treffen Sie diese Änderung also bewusst.
3. Die Assertion „selectedIndex außerhalb des Bereichs“
Symptom: Wenn die Zielliste je nach Nutzerrolle kürzer wird, stürzt die App mit einer Assertion zu selectedIndex ab.
Ursache: Der gespeicherte Index (etwa 3) existiert in der neuen Liste nicht mehr; NavigationBar verlangt einen Index zwischen 0 und destinations.length - 1.
Lösung: Den Index bei jeder Änderung der Liste begrenzen: _selectedIndex = _selectedIndex.clamp(0, destinations.length - 1).
4. Beide Generationen in einer App gemischt
Symptom: Auf einem Screen eine NavigationBar, in einer anderen Shell eine BottomNavigationBar; der Nutzer sieht beim Wechsel zwei verschiedene Höhen und Hervorhebungsstile.
Lösung: Die untere Navigation sollte in einem einzigen Shell-Widget leben. Migrieren Sie diese eine Shell in einem Zug statt Screen für Screen.
Mini-Szenario: NavigationBar auf dem Smartphone, NavigationRail auf dem Tablet
Der eigentliche Gewinn von NavigationBar zeigt sich auf breiten Bildschirmen. Wir definieren die Zielliste einmal und übergeben sie auf schmalen Bildschirmen an eine untere Leiste, auf breiten an eine seitliche Rail; gewählter Index und Seiten werden geteilt. Wie Sie den Breakpoint wählen, steht im Breakpoint-Abschnitt des Beitrags zu LayoutBuilder.
class AdaptiveShell extends StatefulWidget {
const AdaptiveShell({super.key});
@override
State<AdaptiveShell> createState() => _AdaptiveShellState();
}
class _AdaptiveShellState extends State<AdaptiveShell> {
int _selectedIndex = 0;
static const _destinations = [
(icon: Icons.home_outlined, selectedIcon: Icons.home, label: 'Start'),
(icon: Icons.search, selectedIcon: Icons.search, label: 'Suche'),
(icon: Icons.person_outline, selectedIcon: Icons.person, label: 'Profil'),
];
static const List<Widget> _pages = [HomePage(), SearchPage(), ProfilePage()];
void _select(int i) => setState(() => _selectedIndex = i);
@override
Widget build(BuildContext context) {
final wide = MediaQuery.sizeOf(context).width >= 600;
final body = IndexedStack(index: _selectedIndex, children: _pages);
if (wide) {
return Scaffold(
body: Row(
children: [
NavigationRail(
selectedIndex: _selectedIndex,
onDestinationSelected: _select,
labelType: NavigationRailLabelType.all,
destinations: [
for (final d in _destinations)
NavigationRailDestination(
icon: Icon(d.icon),
selectedIcon: Icon(d.selectedIcon),
label: Text(d.label),
),
],
),
const VerticalDivider(width: 1),
Expanded(child: body),
],
),
);
}
return Scaffold(
body: body,
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: _select,
destinations: [
for (final d in _destinations)
NavigationDestination(
icon: Icon(d.icon),
selectedIcon: Icon(d.selectedIcon),
label: d.label,
),
],
),
);
}
}Die Ziele liegen als Dart-3-Records in einer einzigen konstanten Liste; beide Widgets werden daraus erzeugt, sodass ein neuer Tab eine Einzeiler-Änderung ist. Das Detail, auf das es ankommt: Der IndexedStack ist in beiden Zweigen dasselbe Objekt. Die State-Objekte der Seiten überleben den Wechsel zwischen Smartphone- und Tablet-Layout (etwa beim Ändern der Fenstergröße), weil sich Position und Typ des Widgets im Baum nicht ändern. NavigationRail will sein label als Widget, NavigationDestination als String; ein kleiner Unterschied, der aber einen Kompilierfehler erzeugt.
Häufig gestellte Fragen
Ist BottomNavigationBar veraltet? Muss ich migrieren?
Nein, das klassische Widget funktioniert weiter und wurde nicht entfernt. In neuen Material-3-Projekten ist NavigationBar die natürliche Wahl; eine funktionierende Material-2-App muss allein deshalb nicht umgezogen werden.
Wie ändere ich die Farbe des gewählten Icons in NavigationBar?
Ein Gegenstück zu selectedItemColor gibt es nicht. Geben Sie in ThemeData.navigationBarTheme den Feldern iconTheme und labelTextStyle über WidgetStateProperty.resolveWith Werte, die von WidgetState.selected abhängen; für die Pille verwenden Sie indicatorColor.
Hat NavigationBar einen shifting-Typ?
Nein. Das Ausblenden der Beschriftungen erledigt labelBehavior: onlyShowSelected; Hintergrundfarben pro Element gibt es in Material 3 nicht.
Ändert sich die Logik mit IndexedStack und State bei der Migration?
Nein. Der gewählte Index lebt weiterhin im State, die Seiten werden weiterhin mit IndexedStack bewahrt; nur die Parameternamen der Leiste ändern sich.
Verwandte Artikel
Flutter: BottomNavigationBar verwenden
Flutter BottomNavigationBar: currentIndex im State, Seitenzustand mit IndexedStack, fixed vs. shifting und ein Warenkorb-Badge.
Flutter: AppBar-Widget und Anpassung
AppBar in Flutter richtig einsetzen: title, leading, actions und bottom, der Farbwechsel beim Scrollen in Material 3 und Lösungen für häufige Fehler.
Flutter: Helles und dunkles Theme mit ThemeData
Ein Flutter-Theme mit ThemeData aufbauen: ColorScheme.fromSeed, helles und dunkles Theme, ThemeMode, Theme.of, TextTheme, Komponenten-Themes, ThemeExtension.