İçeriğe geç / Skip to content / Zum Inhalt

Navigation und Datenübergabe zwischen Seiten in Flutter

Ahmet Balaman

Zuletzt aktualisiert:

7 Min. Lesezeit

FlutterNavigatorRoutingNavigationState Management
Navigation und Datenübergabe zwischen Seiten in Flutter

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:

Der Navigator-Stapel: am Anfang nur die Startseite, nach push die Detailseite oben, nach pop wieder die Startseite

// 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 page

Wechsel mit pushReplacement()

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => const HomePage()),
);
// Back button goes to the page before login, skipping login

Praxisbeispiel: 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(),
      },
    );
  }
}
// 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']}'),
      ),
    );
  }
}

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 canPop false ist, schließt eine Zurück-Geste die Seite nicht; stattdessen wird onPopInvokedWithResult mit didPop: false aufgerufen.
  • Tippt die Nutzerin auf „Ja“, schließen wir die Seite selbst mit Navigator.pop(context). Dieser Aufruf löst onPopInvokedWithResult erneut aus, diesmal mit didPop: true; deshalb steht if (didPop) return; ganz oben.
  • Die Prüfung context.mounted nach dem await deckt 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 pushReplacement verwenden
  • Beim Abmelden den Stapel mit pushAndRemoveUntil leeren

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.

Kommentare