Navigation - wurzelsand/flutter-memos GitHub Wiki

Navigation

Push und Pop von MaterialPageRoute

Vom Homescreen über ein Button ein Popup-Window öffnen.

Ausführung

import 'package:flutter/material.dart';

void main() {
  runApp(const MyNavigationApp());
}

class MyNavigationApp extends StatelessWidget {
  const MyNavigationApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: MyHomeScreen());
  }
}

class MyHomeScreen extends StatelessWidget {
  const MyHomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('MyHomeScreen')),
      body: Center(
        child: TextButton(
            onPressed: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) => const MyDetailScreen()),
                ),
            child: const Text('Open Popup-Window')),
      ),
    );
  }
}

class MyDetailScreen extends StatelessWidget {
  const MyDetailScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('MyDetailScreen')),
      body: Center(
        child: TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('Close Popup-Window'),
        ),
      ),
    );
  }
}

Returning data

flutter.dev/cookbook/returning-data

import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      title: 'Returning Data',
      home: HomeScreen(),
    ),
  );
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Returning Data Demo'),
      ),
      body: const Center(
        child: SelectionButton(),
      ),
    );
  }
}

class SelectionButton extends StatefulWidget {
  const SelectionButton({super.key});

  @override
  State<SelectionButton> createState() => _SelectionButtonState();
}

class _SelectionButtonState extends State<SelectionButton> {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        _navigateAndDisplaySelection(context);
      },
      child: const Text('Pick an option, any option!'),
    );
  }

  // A method that launches the SelectionScreen and awaits the result from
  // Navigator.pop.
  Future<void> _navigateAndDisplaySelection(BuildContext context) async {
    // Navigator.push returns a Future that completes after calling
    // Navigator.pop on the Selection Screen.
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const SelectionScreen()),
    );

    // added this myself:
    if (result == null) return;

    // When a BuildContext is used from a StatefulWidget, the mounted property
    // must be checked after an asynchronous gap.
    if (!mounted) return;

    // After the Selection Screen returns a result, hide any previous snackbars
    // and show the new result.
    ScaffoldMessenger.of(context)
      ..removeCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text('$result')));
  }
}

class SelectionScreen extends StatelessWidget {
  const SelectionScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Pick an option'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: ElevatedButton(
                onPressed: () {
                  // Close the screen and return "Yep!" as the result.
                  Navigator.pop(context, 'Yep!');
                },
                child: const Text('Yep!'),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: ElevatedButton(
                onPressed: () {
                  // Close the screen and return "Nope." as the result.
                  Navigator.pop(context, 'Nope.');
                },
                child: const Text('Nope.'),
              ),
            )
          ],
        ),
      ),
    );
  }
}

pushNamed

Ausführung

import 'package:flutter/material.dart';

void main() {
  runApp(const MyNavigationApp());
}

class MyNavigationApp extends StatelessWidget {
  const MyNavigationApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      routes: {
        '/': (ctx) => const MyHomeScreen(),
        '/details': (ctx) => const MyDetailScreen(),
      },
    );
  }
}

class MyHomeScreen extends StatelessWidget {
  const MyHomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('MyHomeScreen')),
      body: Center(
        child: TextButton(
            onPressed: () => Navigator.pushNamed(
                  context,
                  '/details',
                ),
            child: const Text('Open Popup-Window')),
      ),
    );
  }
}

class MyDetailScreen extends StatelessWidget {
  const MyDetailScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('MyDetailScreen')),
      body: Center(
        child: TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('Close Popup-Window'),
        ),
      ),
    );
  }
}

onGenerateRoute

Für eine Web-App soll es auch möglich sein, die Adresse einer Seite direkt einzugeben.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyNavigationApp());
}

class MyNavigationApp extends StatelessWidget {
  const MyNavigationApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(onGenerateRoute: (settings) {
      if (settings.name == '/') {
        return MaterialPageRoute(builder: (ctx) => const MyHomeScreen());
      }
      
      if (settings.name != null) {
        final settingsName = settings.name!;
        Uri uri = Uri.parse(settingsName);
        if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'details') {
          final id = uri.pathSegments[1];
          return MaterialPageRoute(builder: (ctx) => MyDetailScreen(id: id));
        }
      }

      return MaterialPageRoute(builder: (ctx) => const MyUnknownScreen());
    });
  }
}

class MyHomeScreen extends StatelessWidget {
  const MyHomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('MyHomeScreen')),
      body: Center(
        child: TextButton(
            onPressed: () => Navigator.pushNamed(
                  context,
                  '/details/1',
                ),
            child: const Text('Open Popup-Window')),
      ),
    );
  }
}

class MyDetailScreen extends StatelessWidget {
  const MyDetailScreen({Key? key, required this.id}) : super(key: key);

  final String id;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('MyDetailScreen/$id')),
      body: Center(
        child: TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('Close Popup-Window'),
        ),
      ),
    );
  }
}

class MyUnknownScreen extends StatelessWidget {
  const MyUnknownScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('MyUnknownScreen')),
      body:
          Center(child: TextButton(onPressed: () => Navigator.pop(context), child: const Text('Close Unknown Screen'))),
    );
  }
}

Navigator 2.0

Die vorherige App kann bereits Eingaben in die Adressleiste so umsetzen, dass die passende Seite angezeigt wird (aber nicht umgekehrt). Ich möchte die App außerdem über den History-Button des Bowsers vor- und zurücksteuern können. Dazu schreibe ich eine möglichst minimal gehaltene App mit dem Navigator-2.0-Framework: Es besteht aus einem Detail-Screen und einem Home-Screen das mit einem Button zum Detail-Screen wechseln kann. Ein Unknown-Screen springt ein, wenn ich eine ungültige Adresse in den Browser eingebe.

Ausführung

import 'package:flutter/material.dart';

class MyConfiguration {
  const MyConfiguration.details()
      : isDetail = true,
        isUnknown = false;

  const MyConfiguration.unknown()
      : isDetail = false,
        isUnknown = true;

  const MyConfiguration.home()
      : isDetail = false,
        isUnknown = false;

  final bool isDetail;
  final bool isUnknown;
}

class MyRouteParser extends RouteInformationParser<MyConfiguration> {
  const MyRouteParser() : super();

  @override
  Future<MyConfiguration> parseRouteInformation(
      RouteInformation routeInformation) async {
    final location = routeInformation.location!;
    final uri = Uri.parse(location);

    if (uri.pathSegments.isEmpty) {
      return const MyConfiguration.home();
    }

    if (uri.pathSegments.length == 1 && uri.pathSegments.first == 'details') {
      return const MyConfiguration.details();
    }

    return const MyConfiguration.unknown();
  }

  @override
  RouteInformation? restoreRouteInformation(MyConfiguration configuration) {
    if (configuration.isDetail) {
      return const RouteInformation(location: '/details');
    }
    if (configuration.isUnknown) {
      return const RouteInformation(location: '/404'); // #1
    }
    return const RouteInformation(location: '/');
  }
}

class MyRouterDelegate extends RouterDelegate<MyConfiguration>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin {
  @override
  MyConfiguration currentConfiguration = const MyConfiguration.unknown();

  @override
  final navigatorKey = GlobalKey<NavigatorState>();

  @override
  Future<void> setNewRoutePath(MyConfiguration configuration) async {
    currentConfiguration = configuration; // #2
  }

  @override
  Widget build(BuildContext context) {
    final homePage = MaterialPage(
        key: const ValueKey('home'),
        child: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: TextButton(
              onPressed: () {
                currentConfiguration = const MyConfiguration.details();
                notifyListeners();
              },
              child: const Text('Show Details')),
        ));

    final detailPage = MaterialPage(
        key: const ValueKey('details'),
        child: Scaffold(
          appBar: AppBar(title: const Text('Details')),
        ));

    final unknownPage = MaterialPage(
        key: const ValueKey('unknown'),
        child: Scaffold(
          appBar: AppBar(title: const Text('Unknown')),
        ));

    return Navigator(
      key: navigatorKey,
      pages: [
        homePage,
        if (currentConfiguration.isDetail)
          detailPage
        else if (currentConfiguration.isUnknown)
          unknownPage,
      ],
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }
        currentConfiguration = const MyConfiguration.home();
        notifyListeners(); // #3
        return true;
      },
    );
  }
}

void main() {
  runApp(MaterialApp.router(
      title: 'Navigator 2.0',
      routeInformationParser: const MyRouteParser(),
      routerDelegate: MyRouterDelegate()));
}

Anmerkungen

  • Ich erzeuge die MaterialApp mit dessen named Constructor .route: Ich übergebe ihm ein RouteInformationParser und ein RouterDelegate.

  • Ich benötige außerdem einen frei definierbaren Datentyp für sogenannte Configuration-Objekte, um Informationen zwischen dem RouterInformationParser und dem RouterDelegate auszutauschen.

  • RouterInformationParser:

    • parseRouteInformation liest die Adressleiste des Browsers aus und gibt dem System das entsprechende Configuration-Objekt zurück.

    • restoreRouteInformation erhält ein Configuration-Objekt und gibt dem Browser Informationen für dessen Adressleiste zurück.

  • RouterDelegate:

    • setNewRoutePath erhält ein Configuration-Objekt und konfiguriert damit seine Variablen um damit die passende Seite aufzubauen.

    • build baut die Seite entsprechend seiner Variablen auf.

    • currentConfiguration gibt entsprechend seiner Variablen ein Configuration-Objekt zurück.

    • navigatorKey wird einmal zu Beginn erstellt und wird vom Navigator-Objekt der build-Funktion benötigt.

  1. return null scheint mir die einzige Möglichkeit zu sein, um nicht in einer 404-Schleife steckenzubleiben.

  2. notifyListeners ist hier nicht nötig, denn nach setNewRoutePath ruft das System eigenständig build auf.

  3. Hier scheint es zunächst so, als könne man auf notifyListeners verzichten: onPopPage wird aufgerufen, wenn ich die Back-Taste der App-Bar drücke und das darunter liegende Page erscheint wieder, ohne dass ein erneutes build notwendig ist. In der Zwischenzeit könnte sich aber das Aussehen der Page, das wieder erscheinen soll, verändert haben.

go_router

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}

final _router = GoRouter(routes: [
  GoRoute(
    path: '/',
    builder: (context, state) => const HomePage(),
    routes: [
      GoRoute(
        path: 'pageA',
        builder: (context, state) => const PageA(),
        routes: [
          GoRoute(
            path: 'pageX',
            builder: (context, state) => const PageAX(),
          ),
          GoRoute(
            path: 'pageY',
            builder: (context, state) => const PageAY(),
          ),
        ],
      ),
      GoRoute(
        path: 'pageB',
        builder: (context, state) => const PageB(),
      )
    ],
  ),
]);

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('HomePage')),
      body: Column(children: [
        TextButton(
          onPressed: () => context.go('/pageA'),
          child: const Text('to pageA'),
        ),
        TextButton(
          onPressed: () => context.go('/pageB'),
          child: const Text('to pageB'),
        ),
      ]),
    );
  }
}

class PageA extends StatelessWidget {
  const PageA({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('PageA')),
      body: Column(children: [
        TextButton(
          onPressed: () => context.go('/pageA/pageX'),
          child: const Text('to pageX'),
        ),
        TextButton(
          onPressed: () => context.go('/pageA/pageY'),
          child: const Text('to pageY'),
        ),
      ]),
    );
  }
}

class PageAX extends StatelessWidget {
  const PageAX({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('PageAX')),
    );
  }
}

class PageAY extends StatelessWidget {
  const PageAY({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('PageAY')),
    );
  }
}

class PageB extends StatelessWidget {
  const PageB({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('PageB')),
    );
  }
}

Send and receive queryParameters via go_router.

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}

final _router = GoRouter(routes: [
  GoRoute(
    path: '/',
    builder: (context, state) => HomePage(state: state),
    routes: [
      GoRoute(
        path: 'pageA',
        builder: (context, state) => const PageA(),
        routes: [
          GoRoute(
            path: 'pageX',
            builder: (context, state) => const PageAX(),
          ),
          GoRoute(
            path: 'pageY',
            builder: (context, state) => const PageAY(),
          ),
        ],
      ),
      GoRoute(
        path: 'pageB',
        builder: (context, state) => const PageB(),
      )
    ],
  ),
]);

class HomePage extends StatelessWidget {
  const HomePage({required this.state, super.key});

  final GoRouterState state;

  @override
  Widget build(BuildContext context) {
// >>>
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      String? param = state.queryParams['param'];
      if (param != null) {
        final snackBar = SnackBar(content: Text(param));
        ScaffoldMessenger.of(context).showSnackBar(snackBar);
      }
    });
// <<<
    return Scaffold(
      appBar: AppBar(title: const Text('HomePage')),
      body: Column(children: [
        TextButton(
          onPressed: () => context.go('/pageA'),
          child: const Text('to pageA'),
        ),
        TextButton(
          onPressed: () => context.go('/pageB'),
          child: const Text('to pageB'),
        ),
      ]),
    );
  }
}

class PageA extends StatelessWidget {
  const PageA({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('PageA')),
      body: Column(children: [
        TextButton(
          onPressed: () => context.go('/pageA/pageX'),
          child: const Text('to pageX'),
        ),
        TextButton(
          onPressed: () => context.go('/pageA/pageY'),
          child: const Text('to pageY'),
        ),
      ]),
    );
  }
}

class PageAX extends StatelessWidget {
  const PageAX({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('PageAX')),
    );
  }
}

class PageAY extends StatelessWidget {
  const PageAY({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('PageAY')),
    );
  }
}

class PageB extends StatelessWidget {
  const PageB({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('PageB')),
// >>>
      body: TextButton(
        onPressed: () {
          final uri = Uri(
            path: '/',
            queryParameters: <String, String?>{'param': '42'},
          );
          context.go(uri.toString());
        },
        child: const Text('Return 42'),
      ),
// <<<
    );
  }
}
  • You cannot call showSnackBar within build. Therefore I used addPostFrameCallback.
⚠️ **GitHub.com Fallback** ⚠️