artodeto am :
Lieber Onli,
danke für den Artikel. Hätte, hätte, Fahrradkette ... hätte ich mehr Zeit, würde ich das Wissen direkt ausprobieren. Jucken tut es einem in den Fingern direkt, nachdem man deinen letzten Satz gelesen hat.
Wie man in Flutter von einer Seite zur nächsten wechselt ist nicht ganz einfach, obwohl es eigentlich doch ganz simpel ist. Der Navigationsaufruf ist schnell geschrieben. Doch primär definiert man keine Seiten, sondern Widgets, und ob die jetzt alleine angezeigt werden oder nur als Teil eines anderen Widgets hängt einzig von ihrer Verwendung ab – schon das ist verwirrend. Dazu gibt es nicht den einen Weg, das Routing aufzusetzen, sondern viele: Das Flutter-Projekt selbst kennt mit dem Navigator (1.0), dem Router (2.0) und jetzt mit dem go_router (2.0+ ?) direkt drei offizielle Möglichkeiten, wobei sich hinter dem Navigator gleich mehrere Möglichkeiten verbergen und der Router 2.0 komplett vage ist. Dazu kommen die vielen Routing-Plugins auf pub.dev, alle mit ihren eigenen Vor- und Nachteilen.
Dieser Artikel wird nicht alle diese Möglichkeiten vorstellen. Stattdessen zeige ich einen Weg, wie man völlig ohne Plugins strukturiert die Routen anlegen und ihre Übergänge animieren kann. Dazu gehört dieses Git-Repo, in dem der Code komplett nachvollzogen werden kann.
Ich werde hier mit einer einfachen Flutter-Anwendung mit einer main.dart und drei Widgets view[123].dart benutzen. Die drei Widgets sollen jeweils als eigene Seiten aufgerufen werden. Sie sind so definiert:
import 'package:flutter/material.dart'; class View2 extends StatelessWidget { const View2({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('View 2')), body: const Center( child: Text('View 2'), ), ); } }
Und der Startbildschirm zeigt drei Buttons untereinander in einer Reihe:
class MyHomePage extends StatelessWidget { final String title; const MyHomePage({super.key, required this.title}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(title)), body: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Center( child: Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( onPressed: () => null, child: const Text('View 1')), ), ), Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( onPressed: () => null, child: const Text('View 2')), ), Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( onPressed: () => null, child: const Text('View 3')), ), ], ), ); } }
Diese drei Buttons sollen nun jeweils zu ihrem Widget navigieren.
Schauen wir uns also erstmal an, wie einfache NamedRoutes funktionieren. Den Namen entsprechend bekommt hier eine Route einen Namen und wird darüber aufgerufen, ähnlich einer URL. Man erstellt dafür eine routes.dart mit einem solchen Inhalt:
import 'package:flutter/material.dart'; import 'package:flutter_navigation/view1.dart'; import 'package:flutter_navigation/view2.dart'; import 'package:flutter_navigation/view3.dart'; final routes = { '/view1': (BuildContext context) => const View1(), '/view2': (BuildContext context) => const View2(), '/view3': (BuildContext context) => const View3(), };
Die muss nun der Flutteranwendung zugewiesen werden:
class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), routes: routes, // genau hier ); } }
Und schon können die Buttons eine Funktion bekommen:
ElevatedButton( onPressed: () => Navigator.of(context).pushNamed('/view2'), child: const Text('View 2'), )
Navigator.of(context)
holt sich den Navigator, pushNamed
navigiert zum zuvor angelegten Widget. Mit einem Navigator.of(context).pop()
würde man wieder zurückkommen, es gibt hier also einen Navigations-Stack.
Was aber, wenn bei der Navigation auch Daten an das Widget gegeben werden sollen?
Dafür gibt es Argumente. Um genau zu sein gibt es ein Arguments-Objekt, in das alles beliebige gespeichert werden kann. Zum Beispiel hier eine Map mit einem String, den das Widget dann anzeigen soll:
ElevatedButton( onPressed: () => Navigator.of(context).pushNamed('/view1', arguments: { 'content': 'Dynamic text', }),
Damit das Widget es auch bekommt, müssen wir seine Route in der routes.dart ändern:
'/view1': (BuildContext context) { final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>; return View1( content: args['content']!, ); },
Wir machen uns hier zunutze, dass Dart (zumindest seit der Version für Flutter 3) mit den Typen recht flexibel umgehen kann, sodass content
nicht manuell in einen String umgewandelt werden muss. Das Widget kann mit dem Parameter direkt arbeiten:
import 'package:flutter/material.dart'; class View1 extends StatelessWidget { final String content; const View1({super.key, required this.content}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('View 1'), ), body: Center( child: Text(content), ), ); } }
Das Widget kann nun dynamisch bei jedem Navigationsaufruf mit einem anderen Text befüllt werden.
Schön an diesem Ansatz ist der geringe Aufwand. Es braucht keine eigene Klasse für die Widget-Argumente, weil sie einfach in eine Map gepackt werden. Und da bei der Map der Value-Typ auf dynamic
gesetzt wurde kann beliebiges übertragen werden. Eine automatische Codegenerierung wie bei auto_route hier draufzusetzen scheint unnötig.
Die Animationen anpassen zu können dagegen wird für manche Anwendungen nötig sein. Ich zeige einen erweiterbaren Ansatz. Er wird mit onGenerateRoute
der MaterialApp arbeiten.
Das ist jetzt viel auf einmal:
return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'Flutter Demo Home Page'), onGenerateRoute: (settings) { if (routes.containsKey(settings.name)) { final args = settings.arguments as Map<String, dynamic>?; return PageRouteBuilder( settings: settings, pageBuilder: (context, animation, secondaryAnimation) => routes[settings.name]!(context), transitionsBuilder: (context, animation, secondaryAnimation, child) { switch (args?['transition']) { case Transitions.scale: return ScaleTransition(scale: animation, child: child); case Transitions.fade: return FadeTransition(opacity: animation, child: child); default: return SlideTransition( position: Tween<Offset>( begin: const Offset(0.0, 1.0), end: Offset.zero, ).animate(animation), child: child, ); } }); } // Unknown route return MaterialPageRoute(builder: (context) => Container()); }, );
Also, was ist hier passiert:
Zuerst wurde der routes:
-Parameter entfernt. Wäre er noch da, hätte er Priorität und unser neuer Code in onGenerateRoute
würde ignoriert.
Der nächste Kniff ist pageBuilder: (context, animation, secondaryAnimation) => routes[settings.name]!(context)
. Hiermit wird beim Seitenbau in routes
nach einer Route gesucht. So kann die alte routes.dart weiterbenutzt werden, sie muss nichtmal editiert werden. Gut so, denn dadurch behalten wir einen festen Ort für übersichtliche Routendefinitionen.
Es folgt der transitionsBuilder
. Der schaut, ob bei dieser Navigation das Argument transition
übergeben wurde. Wenn ja und es einen bestimmten Wert hat, wird eine der vordefinierten Übergangsanimationen gesetzt. Dafür wurde ein bislang nicht gezeigtes Enum angelegt:
enum Transitions { scale, fade }
Die Navigation mit Animationsauswahl sähe nun nicht viel anders aus als zuvor:
ElevatedButton( onPressed: () => Navigator.of(context).pushNamed('/view1', arguments: { 'content': 'Dynamic text', 'transition': Transitions.scale }), child: const Text('View 1')), ),
Das ginge natürlich auch anders – so hatte ich eine Variante mit dem Plugin page_transition entwickelt, die brauchte aber Änderungen an der routes.dart.
Dieser Artikel und Ansatz ist ein Nebenprodukt meiner Analyse von Flutters Routingsituation. Die vielen Lösungen waren chaotisch präsentiert, viele erschienen mir auch unnötig kompliziert. NamedRoutes stachen direkt als klar verständliche Lösung heraus, aber wie sie gut zu benutzen sind, samt Argumenten und Animationen, sah ich nicht erklärt. Ob sie dazu überhaupt taugen? Sie tun es, wie hoffentlich deutlich wurde.
Laut Dokumentation sollen NamedRoutes Nachteile haben – bei Push-Benachrichtigungen würden sie immer ihr Ziel öffnen, selbst wenn es schon offen sei (ist das in der onGenerateRoute wirklich nicht abfangbar?) und bei Webanwendungen als Kompilierziel der Vorwärtsbutton im Browser mit ihnen nicht funktionieren (kein Problem für mich, da ich Flutter für Webanwendungen ungeeignet finde). Wer darüber stolpern würde sollte sich wahrscheinlich als zweitbeste Lösung den go_router ansehen.
Lieber Onli,
danke für den Artikel. Hätte, hätte, Fahrradkette ... hätte ich mehr Zeit, würde ich das Wissen direkt ausprobieren. Jucken tut es einem in den Fingern direkt, nachdem man deinen letzten Satz gelesen hat.
Danke für das tolle Feedback.
Um Salz in die Wunde zu streuen ;) : Flutter hat mittlerweile Linux als Kompilierziel aktiviert und das funktioniert überraschend problemlos. Nichts mehr mit Snapanforderungen, wie es in der Alpha noch war. Einfach entwickeln und wenn kein Emulator offen ist, startet eben ein Linuxprogramm mit dem Code darin. So wenig ich von Flutter für die meisten Webseiten halte, so attraktiv erscheint es mir als Option für einige Linuxprogramme (es ist dank seines Ansatzes soo viel einfacher als wxWidgets/GTK/Qt).
Eben genau dieses Szenario lässt mich immer um Flutter schwirren.
Mal eben etwas für $Frau bauen um die Eingabedaten in meinem Wunschformat zu haben ;-).
artodeto's blog about coding, politics and the world am : Die KW 46/2022 im Link-Rückblick
Vorschau anzeigen
onli blogging am : Ein kurzer Blogrückblick auf 2022
Vorschau anzeigen