Navigation und Datenübergabe zwischen Seiten in Flutter
Zuletzt aktualisiert:
7 Min. Lesezeit

Wer aus der Webentwicklung kommt, fragt in Flutter meist als Erstes: „Wenn es keine Links gibt, wie wechselt man dann zwischen Seiten?“ Die Antwort ist der Navigator.
In Flutter dient die Klasse Navigator dem Seitenwechsel. Sie arbeitet wie ein Stapel: Sie öffnen eine Seite, sie wird oben aufgelegt; Sie gehen zurück, die oberste Seite wird entfernt. Falls Widget-Baum, Scaffold und BuildContext noch nicht sitzen, lesen Sie zuerst den Beitrag zu Widget-Baum und Layout – alles Weitere hier baut darauf auf.
Live-Demo: der Navigator im Einsatz
Probieren Sie Seitenwechsel und Datenübergabe interaktiv aus:
💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.
Grundlegende Verwendung des Navigator
Der einfachste Weg, von einer Seite zur nächsten zu wechseln:
// Home page
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home Page')),
body: Center(
child: ElevatedButton(
child: Text('Go to Detail Page'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
);
},
),
),
);
}
}
// Detail page
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail Page')),
body: Center(
child: Text('This is the detail page'),
),
);
}
}Navigator.push() öffnet eine neue Seite. MaterialPageRoute liefert die Animation für den Seitenwechsel.
Zurückgehen – pop()
Es gibt zwei Wege zurück:
1. Der automatische Zurück-Button
Wenn Sie eine AppBar verwenden, fügt Flutter automatisch einen Zurück-Button hinzu. Nutzer tippen darauf, um zurückzukehren.
2. Programmatisch zurückgehen
ElevatedButton(
child: Text('Go Back'),
onPressed: () {
Navigator.pop(context);
},
)Navigator.pop() entfernt die oberste Seite vom Stapel.
Daten zwischen Seiten übergeben
Jetzt wird es spannend. Um Daten an Seiten zu übergeben, verwenden wir Konstruktoren.
Die sendende Seite
class ProductListPage extends StatelessWidget {
const ProductListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: ListView(
children: [
ListTile(
title: Text('iPhone 15'),
subtitle: Text('\$999'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
productName: 'iPhone 15',
price: 999,
description: 'Next generation smartphone',
),
),
);
},
),
ListTile(
title: Text('Samsung S24'),
subtitle: Text('\$899'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
productName: 'Samsung S24',
price: 899,
description: 'The best of Android',
),
),
);
},
),
],
),
);
}
}Die empfangende Seite
class ProductDetailPage extends StatelessWidget {
final String productName;
final int price;
final String description;
const ProductDetailPage({
super.key,
required this.productName,
required this.price,
required this.description,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(productName)),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
productName,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'\$${price.toString()}',
style: TextStyle(fontSize: 20, color: Colors.green),
),
SizedBox(height: 16),
Text(
description,
style: TextStyle(fontSize: 16),
),
],
),
),
);
}
}Mit required im Konstruktor machen wir Parameter verpflichtend. So müssen diese Angaben beim Öffnen der Seite übergeben werden.
Beim Zurückgehen Daten zurückgeben
Manchmal möchten Sie von der geöffneten Seite Daten zurückerhalten – etwa wenn die Nutzerin etwas ausgewählt hat und Sie diese Auswahl an die vorherige Seite übergeben wollen.
Die Seite, die Daten anfordert
class SelectCityPage extends StatelessWidget {
const SelectCityPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Select City')),
body: Center(
child: ElevatedButton(
child: Text('Choose City'),
onPressed: () async {
// Open page and wait for result
final selectedCity = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const CityListPage()),
);
// The page may have closed while we waited; check before using context
if (!context.mounted) return;
// Result received
if (selectedCity != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Selected: $selectedCity')),
);
}
},
),
),
);
}
}Die Seite, die Daten zurückgibt
class CityListPage extends StatelessWidget {
const CityListPage({super.key});
static const List<String> cities = [
'New York',
'Los Angeles',
'Chicago',
'Houston',
'Phoenix',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('City List')),
body: ListView.builder(
itemCount: cities.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(cities[index]),
onTap: () {
// Send selected city back
Navigator.pop(context, cities[index]);
},
);
},
),
);
}
}Mit Navigator.pop(context, value) übergeben wir die Daten als zweiten Parameter. Mit Navigator.push<String> legen wir außerdem den Typ des Rückgabewerts fest; verlässt die Nutzerin die Seite ohne Auswahl über den Zurück-Button, ist das Ergebnis null.
Achten Sie auf die await-Zeile: Während wir auf das Ergebnis warten, kann die Seite bereits verlassen worden sein. Deshalb prüfen wir nach dem await zuerst context.mounted, bevor wir context verwenden. Flutters Lint-Regel use_build_context_synchronously warnt, wenn diese Prüfung fehlt.
pushReplacement – das Zurückgehen verhindern
Manchmal soll die Nutzerin nicht mit dem Zurück-Button auf die vorherige Seite gelangen. Ein typisches Beispiel: nach dem Login nicht zurück zur Anmeldeseite.
Normaler Wechsel mit push()
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
// User can press back to return to login pageWechsel mit pushReplacement()
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
// Back button goes to the page before login, skipping loginPraxisbeispiel: nach dem Login
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
void _login(BuildContext context) {
// Login successful
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
child: Text('Login'),
onPressed: () => _login(context),
),
),
);
}
}Wenn Sie nun auf der HomePage zurückgehen, landen Sie nicht wieder auf der LoginPage, sondern verlassen die App.
Beispiel: nach der Registrierung
class RegisterPage extends StatelessWidget {
const RegisterPage({super.key});
void _register(BuildContext context) {
// Registration successful
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Registration successful! You can now login.')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Register')),
body: Center(
child: ElevatedButton(
child: Text('Complete Registration'),
onPressed: () => _register(context),
),
),
);
}
}pushAndRemoveUntil – den gesamten Stapel leeren
Manchmal möchten Sie nicht nur eine Seite, sondern den gesamten Stapel entfernen.
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
(route) => false, // Remove all pages
);Das nutzt man, um Nutzer in einen völlig neuen Ablauf zu führen. Beim Abmelden etwa werden alle Seiten entfernt und nur der Anmeldebildschirm bleibt übrig.
Beispiel: Abmelden
void logout(BuildContext context) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
(route) => false,
);
}Benannte Routen
In größeren Projekten ist es mühsam, jedes Mal MaterialPageRoute zu schreiben. Dafür gibt es benannte Routen.
Hinweis: Die offizielle Flutter-Dokumentation empfiehlt benannte Routen für die meisten Apps nicht. Ihr Verhalten bei Deep Links lässt sich nicht anpassen, und im Web wird der Vorwärts-Button des Browsers nicht unterstützt. Wenn Sie das brauchen, ist ein Routing-Paket wie go_router die bessere Wahl. In bestehenden Projekten begegnen Ihnen benannte Routen trotzdem häufig, daher lohnt es sich zu wissen, wie sie funktionieren.
Routen in der main.dart definieren
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/detail': (context) => const DetailPage(),
'/profile': (context) => const ProfilePage(),
'/settings': (context) => const SettingsPage(),
'/login': (context) => const LoginPage(),
},
);
}
}Navigation über benannte Routen
// Normal navigation
Navigator.pushNamed(context, '/detail');
// Prevent back navigation
Navigator.pushReplacementNamed(context, '/');
// Clear stack
Navigator.pushNamedAndRemoveUntil(
context,
'/login',
(route) => false,
);Daten über benannte Routen übergeben
// Sending data
Navigator.pushNamed(
context,
'/detail',
arguments: {
'id': 123,
'name': 'John',
},
);
// Receiving data
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map;
return Scaffold(
appBar: AppBar(title: Text(args['name'])),
body: Center(
child: Text('ID: ${args['id']}'),
),
);
}
}Navigation mit Animation
Sie können unterschiedliche Übergangsanimationen einsetzen. Soll statt der ganzen Seite ein gemeinsames Bild von einer Seite in die nächste wandern, gibt es dafür ein eigenes Werkzeug: die Hero-Animation.
Von rechts einschieben (Standard)
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
);Von unten einschieben
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailPage(),
fullscreenDialog: true,
),
);Eigene Animation
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => const DetailPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(
CurveTween(curve: curve),
);
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);Den Zurück-Button abfangen: PopScope
Um einzugreifen, wenn jemand eine Seite verlassen will (etwa um bei ungespeicherten Änderungen nachzufragen), verwenden Sie PopScope. Der Zurück-Button und die Zurück-Geste unter Android, die Wisch-Geste unter iOS und der Zurück-Pfeil in der AppBar laufen alle darüber:
class MyPage extends StatelessWidget {
const MyPage({super.key});
Future<bool?> _showExitDialog(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Möchten Sie die Seite wirklich verlassen?'),
actions: [
TextButton(
child: const Text('Nein'),
onPressed: () => Navigator.pop(context, false),
),
TextButton(
child: const Text('Ja'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return PopScope<Object?>(
// false: a back gesture does not close the page directly; we are asked first
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) async {
// The page is already gone, nothing to do
if (didPop) return;
final shouldPop = await _showExitDialog(context) ?? false;
if (shouldPop && context.mounted) {
Navigator.pop(context);
}
},
child: Scaffold(
appBar: AppBar(title: const Text('Seite')),
body: const Center(child: Text('Inhalt')),
),
);
}
}So funktioniert es:
- Solange
canPopfalseist, schließt eine Zurück-Geste die Seite nicht; stattdessen wirdonPopInvokedWithResultmitdidPop: falseaufgerufen. - Tippt die Nutzerin auf „Ja“, schließen wir die Seite selbst mit
Navigator.pop(context). Dieser Aufruf löstonPopInvokedWithResulterneut aus, diesmal mitdidPop: true; deshalb stehtif (didPop) return;ganz oben. - Die Prüfung
context.mountednach demawaitdeckt den Fall ab, dass die Seite aus einem anderen Grund geschlossen wurde, während der Dialog offen war.
Ältere Anleitungen verwenden dafür WillPopScope und onWillPop. WillPopScope ist seit Flutter 3.16 veraltet (deprecated), weil es nicht mit der vorausschauenden Zurück-Geste (Predictive Back) von Android funktioniert: Das System muss wissen, ob die Seite geschlossen werden darf, bevor die Geste abgeschlossen ist. PopScope legt diese Entscheidung über canPop im Voraus fest und löst damit das Problem. Aus demselben Grund wurde auch der Parameter onPopInvoked zugunsten von onPopInvokedWithResult als veraltet markiert.
Praktische Tipps
1. Rufen Sie den Navigator mit einem Context unterhalb des Navigators auf
Navigator.push(context, ...) sucht ausgehend vom übergebenen Context nach oben den nächsten Navigator. Der Context in der build-Methode einer Seite ist dafür geeignet; alle Beispiele in diesem Beitrag funktionieren so. Das Problem entsteht, wenn der Context oberhalb des Navigator liegt. Der häufigste Fall: mit dem Context des Widgets navigieren, das MaterialApp erzeugt:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
// ❌ This context belongs to MyApp. MaterialApp creates the Navigator,
// so the Navigator sits below this context.
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
),
child: const Text('Los'),
),
),
),
);
}
}Beim Tippen auf den Button erscheint der Fehler „Navigator operation requested with a context that does not include a Navigator“. Die Lösung: den Button in ein eigenes Widget verschieben, das unterhalb von MaterialApp gebaut wird:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: HomePage());
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
// ✅ This context is below MaterialApp, and therefore below the Navigator
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailPage()),
),
child: const Text('Los'),
),
),
);
}
}Wenn Sie in derselben build-Methode bleiben möchten, funktioniert es auch, den Inhalt von home: in einen Builder zu packen; Builder liefert einen neuen Context unterhalb von MaterialApp. Ein eigenes Seiten-Widget ist aber meist lesbarer. Ein Builder ist also nicht bei jedem Navigator.push nötig, sondern nur, wenn der Context tatsächlich oberhalb des Navigators liegt.
2. async/await nutzen
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const SelectPage()),
);
// The page may have closed while we waited
if (!context.mounted) return;
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Result: $result')),
);
}Navigator.push liefert ein Future, das abgeschlossen wird, sobald die geöffnete Seite per pop geschlossen wird. Gewöhnen Sie sich an, nach einem await zuerst context.mounted zu prüfen, bevor Sie den Context wieder verwenden.
3. Vermeiden Sie unnötige Navigator-Aufrufe
// ❌ Unnecessary - within the same page
Navigator.push(context, MaterialPageRoute(...));
// ✅ Use state update
setState(() {
selectedIndex = 1;
});Jedes push erzeugt für die geöffnete Seite ein neues State-Objekt, jedes pop verwirft es wieder. Wann genau welche Methode läuft, zeige ich Schritt für Schritt im Beitrag zum Lebenszyklus.
Zusammenfassung
Navigation in Flutter:
- push(): Neue Seite öffnen
- pop(): Zurückgehen
- pushReplacement(): Zurückgehen verhindern
- pushAndRemoveUntil(): Stapel leeren
- pushNamed(): Benannte Routen nutzen
- PopScope: Das Zurückgehen steuern (ersetzt das alte
WillPopScope)
Datenübergabe:
- Daten über den Konstruktor nach vorn übergeben
- Daten über
pop(context, data)zurückgeben
Verwaltung des Back-Stacks:
- Nach Login und Registrierung
pushReplacementverwenden - Beim Abmelden den Stapel mit
pushAndRemoveUntilleeren
Der Navigator gehört zu den grundlegendsten Funktionen von Flutter. Wenn Sie sich einmal daran gewöhnt haben, geht er Ihnen in Fleisch und Blut über.
Bei der Navigation hängen geblieben?
Bis zum nächsten Artikel! 🚀
Häufig gestellte Fragen
Worin unterscheiden sich push und pushReplacement?
push legt die neue Seite oben auf den Stapel, die alte bleibt darunter liegen, und der Zurück-Button funktioniert. pushReplacement ersetzt die oberste Seite durch die neue; es bleibt nichts übrig, wohin man zurückgehen könnte. Beim Verlassen von Login- oder Splash-Screens ist die zweite Variante die richtige.
Wie gebe ich beim Zurückgehen Daten an die vorherige Seite?
Rufen Sie auf der schließenden Seite Navigator.pop(context, daten) auf. Auf der aufrufenden Seite liefert Navigator.push(...) ein Future, das Sie mit await auswerten können. Verlässt die Nutzerin die Seite über den Zurück-Button, ist das Ergebnis null – prüfen Sie den Wert also immer.
Benannte Routen oder MaterialPageRoute?
In kleinen und mittleren Apps genügt MaterialPageRoute, und es ist typsicherer. Die offizielle Flutter-Dokumentation empfiehlt benannte Routen für die meisten Apps nicht: Ihr Deep-Link-Verhalten lässt sich nicht anpassen, und im Web fehlt die Unterstützung für den Vorwärts-Button des Browsers. Wenn Sie die Navigation zentral definieren oder Deep Links sauber unterstützen möchten, passt ein Paket wie go_router besser.
Was verwende ich statt WillPopScope?
PopScope. Mit canPop: false halten Sie die Zurück-Geste an, in onPopInvokedWithResult entscheiden Sie und schließen die Seite bei Bedarf selbst mit Navigator.pop(context). WillPopScope ist seit Flutter 3.16 veraltet und funktioniert nicht mit der vorausschauenden Zurück-Geste von Android.
Verwandte Artikel
Flutter: BottomNavigationBar im Vergleich zu NavigationBar
Von BottomNavigationBar zur Material-3-NavigationBar: Zuordnungstabelle, labelBehavior, NavigationBarThemeData, Entscheidungshilfe und adaptive Shell.
Flutter: Modernes State Management mit Riverpod 3
State Management in Flutter mit Riverpod 3: Notifier, AsyncNotifier, ref.watch und ref.read, family, autoDispose und Hinweise zum Umstieg von StateNotifier.
Flutter: State Management mit Provider
State Management mit dem Paket Provider in Flutter: ChangeNotifier, Consumer, MultiProvider und Best Practices.