mirror of https://github.com/flutter/samples.git
Update navigation_and_routing sample to go_router (#2067)
This is a new PR to update the navigation_and_routing sample to the latest version of go_router. It is a based on https://github.com/flutter/samples/pull/1437 --------- Co-authored-by: Brett Morgan <brettmorgan@google.com>pull/2075/head
parent
3e4e3bc784
commit
49eebf8c20
@ -1,8 +0,0 @@
|
|||||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
export 'routing/delegate.dart';
|
|
||||||
export 'routing/parsed_route.dart';
|
|
||||||
export 'routing/parser.dart';
|
|
||||||
export 'routing/route_state.dart';
|
|
@ -1,47 +0,0 @@
|
|||||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import 'parsed_route.dart';
|
|
||||||
import 'route_state.dart';
|
|
||||||
|
|
||||||
class SimpleRouterDelegate extends RouterDelegate<ParsedRoute>
|
|
||||||
with ChangeNotifier, PopNavigatorRouterDelegateMixin<ParsedRoute> {
|
|
||||||
final RouteState routeState;
|
|
||||||
final WidgetBuilder builder;
|
|
||||||
|
|
||||||
@override
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
|
|
||||||
SimpleRouterDelegate({
|
|
||||||
required this.routeState,
|
|
||||||
required this.builder,
|
|
||||||
required this.navigatorKey,
|
|
||||||
}) {
|
|
||||||
routeState.addListener(notifyListeners);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => builder(context);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setNewRoutePath(ParsedRoute configuration) async {
|
|
||||||
routeState.route = configuration;
|
|
||||||
return SynchronousFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ParsedRoute get currentConfiguration => routeState.route;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
routeState.removeListener(notifyListeners);
|
|
||||||
routeState.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:quiver/core.dart';
|
|
||||||
|
|
||||||
import 'parser.dart';
|
|
||||||
|
|
||||||
/// A route path that has been parsed by [TemplateRouteParser].
|
|
||||||
class ParsedRoute {
|
|
||||||
/// The current path location without query parameters. (/book/123)
|
|
||||||
final String path;
|
|
||||||
|
|
||||||
/// The path template (/book/:id)
|
|
||||||
final String pathTemplate;
|
|
||||||
|
|
||||||
/// The path parameters ({id: 123})
|
|
||||||
final Map<String, String> parameters;
|
|
||||||
|
|
||||||
/// The query parameters ({search: abc})
|
|
||||||
final Map<String, String> queryParameters;
|
|
||||||
|
|
||||||
static const _mapEquality = MapEquality<String, String>();
|
|
||||||
|
|
||||||
ParsedRoute(
|
|
||||||
this.path, this.pathTemplate, this.parameters, this.queryParameters);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is ParsedRoute &&
|
|
||||||
other.pathTemplate == pathTemplate &&
|
|
||||||
other.path == path &&
|
|
||||||
_mapEquality.equals(parameters, other.parameters) &&
|
|
||||||
_mapEquality.equals(queryParameters, other.queryParameters);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => hash4(
|
|
||||||
path,
|
|
||||||
pathTemplate,
|
|
||||||
_mapEquality.hash(parameters),
|
|
||||||
_mapEquality.hash(queryParameters),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => '<ParsedRoute '
|
|
||||||
'template: $pathTemplate '
|
|
||||||
'path: $path '
|
|
||||||
'parameters: $parameters '
|
|
||||||
'query parameters: $queryParameters>';
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:path_to_regexp/path_to_regexp.dart';
|
|
||||||
|
|
||||||
import 'parsed_route.dart';
|
|
||||||
|
|
||||||
/// Used by [TemplateRouteParser] to guard access to routes.
|
|
||||||
typedef RouteGuard<T> = Future<T> Function(T from);
|
|
||||||
|
|
||||||
/// Parses the URI path into a [ParsedRoute].
|
|
||||||
class TemplateRouteParser extends RouteInformationParser<ParsedRoute> {
|
|
||||||
final List<String> _pathTemplates;
|
|
||||||
final RouteGuard<ParsedRoute>? guard;
|
|
||||||
final ParsedRoute initialRoute;
|
|
||||||
|
|
||||||
TemplateRouteParser({
|
|
||||||
/// The list of allowed path templates (['/', '/users/:id'])
|
|
||||||
required List<String> allowedPaths,
|
|
||||||
|
|
||||||
/// The initial route
|
|
||||||
String initialRoute = '/',
|
|
||||||
|
|
||||||
/// [RouteGuard] used to redirect.
|
|
||||||
this.guard,
|
|
||||||
}) : initialRoute = ParsedRoute(initialRoute, initialRoute, {}, {}),
|
|
||||||
_pathTemplates = [
|
|
||||||
...allowedPaths,
|
|
||||||
],
|
|
||||||
assert(allowedPaths.contains(initialRoute));
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<ParsedRoute> parseRouteInformation(
|
|
||||||
RouteInformation routeInformation,
|
|
||||||
) async {
|
|
||||||
final uri = routeInformation.uri;
|
|
||||||
final path = uri.toString();
|
|
||||||
final queryParams = uri.queryParameters;
|
|
||||||
var parsedRoute = initialRoute;
|
|
||||||
|
|
||||||
for (var pathTemplate in _pathTemplates) {
|
|
||||||
final parameters = <String>[];
|
|
||||||
var pathRegExp = pathToRegExp(pathTemplate, parameters: parameters);
|
|
||||||
if (pathRegExp.hasMatch(path)) {
|
|
||||||
final match = pathRegExp.matchAsPrefix(path);
|
|
||||||
if (match == null) continue;
|
|
||||||
final params = extract(parameters, match);
|
|
||||||
parsedRoute = ParsedRoute(path, pathTemplate, params, queryParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect if a guard is present
|
|
||||||
var guard = this.guard;
|
|
||||||
if (guard != null) {
|
|
||||||
return guard(parsedRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
RouteInformation restoreRouteInformation(ParsedRoute configuration) =>
|
|
||||||
RouteInformation(uri: Uri.parse(configuration.path));
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import 'parsed_route.dart';
|
|
||||||
import 'parser.dart';
|
|
||||||
|
|
||||||
/// The current route state. To change the current route, call obtain the state
|
|
||||||
/// using `RouteStateScope.of(context)` and call `go()`:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// RouteStateScope.of(context).go('/book/2');
|
|
||||||
/// ```
|
|
||||||
class RouteState extends ChangeNotifier {
|
|
||||||
final TemplateRouteParser _parser;
|
|
||||||
ParsedRoute _route;
|
|
||||||
|
|
||||||
RouteState(this._parser) : _route = _parser.initialRoute;
|
|
||||||
|
|
||||||
ParsedRoute get route => _route;
|
|
||||||
|
|
||||||
set route(ParsedRoute route) {
|
|
||||||
// Don't notify listeners if the path hasn't changed.
|
|
||||||
if (_route == route) return;
|
|
||||||
|
|
||||||
_route = route;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> go(String route) async {
|
|
||||||
this.route = await _parser
|
|
||||||
.parseRouteInformation(RouteInformation(uri: Uri.parse(route)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provides the current [RouteState] to descendant widgets in the tree.
|
|
||||||
class RouteStateScope extends InheritedNotifier<RouteState> {
|
|
||||||
const RouteStateScope({
|
|
||||||
required super.notifier,
|
|
||||||
required super.child,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
static RouteState of(BuildContext context) =>
|
|
||||||
context.dependOnInheritedWidgetOfExactType<RouteStateScope>()!.notifier!;
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../auth.dart';
|
|
||||||
import '../data.dart';
|
|
||||||
import '../routing.dart';
|
|
||||||
import '../screens/sign_in.dart';
|
|
||||||
import '../widgets/fade_transition_page.dart';
|
|
||||||
import 'author_details.dart';
|
|
||||||
import 'book_details.dart';
|
|
||||||
import 'scaffold.dart';
|
|
||||||
|
|
||||||
/// Builds the top-level navigator for the app. The pages to display are based
|
|
||||||
/// on the `routeState` that was parsed by the TemplateRouteParser.
|
|
||||||
class BookstoreNavigator extends StatefulWidget {
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
|
|
||||||
const BookstoreNavigator({
|
|
||||||
required this.navigatorKey,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<BookstoreNavigator> createState() => _BookstoreNavigatorState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
|
||||||
final _signInKey = const ValueKey('Sign in');
|
|
||||||
final _scaffoldKey = const ValueKey('App scaffold');
|
|
||||||
final _bookDetailsKey = const ValueKey('Book details screen');
|
|
||||||
final _authorDetailsKey = const ValueKey('Author details screen');
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final routeState = RouteStateScope.of(context);
|
|
||||||
final authState = BookstoreAuthScope.of(context);
|
|
||||||
final pathTemplate = routeState.route.pathTemplate;
|
|
||||||
|
|
||||||
Book? selectedBook;
|
|
||||||
if (pathTemplate == '/book/:bookId') {
|
|
||||||
selectedBook = libraryInstance.allBooks.firstWhereOrNull(
|
|
||||||
(b) => b.id.toString() == routeState.route.parameters['bookId']);
|
|
||||||
}
|
|
||||||
|
|
||||||
Author? selectedAuthor;
|
|
||||||
if (pathTemplate == '/author/:authorId') {
|
|
||||||
selectedAuthor = libraryInstance.allAuthors.firstWhereOrNull(
|
|
||||||
(b) => b.id.toString() == routeState.route.parameters['authorId']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Navigator(
|
|
||||||
key: widget.navigatorKey,
|
|
||||||
onPopPage: (route, dynamic result) {
|
|
||||||
// When a page that is stacked on top of the scaffold is popped, display
|
|
||||||
// the /books or /authors tab in BookstoreScaffold.
|
|
||||||
if (route.settings is Page &&
|
|
||||||
(route.settings as Page).key == _bookDetailsKey) {
|
|
||||||
routeState.go('/books/popular');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route.settings is Page &&
|
|
||||||
(route.settings as Page).key == _authorDetailsKey) {
|
|
||||||
routeState.go('/authors');
|
|
||||||
}
|
|
||||||
|
|
||||||
return route.didPop(result);
|
|
||||||
},
|
|
||||||
pages: [
|
|
||||||
if (routeState.route.pathTemplate == '/signin')
|
|
||||||
// Display the sign in screen.
|
|
||||||
FadeTransitionPage<void>(
|
|
||||||
key: _signInKey,
|
|
||||||
child: SignInScreen(
|
|
||||||
onSignIn: (credentials) async {
|
|
||||||
var signedIn = await authState.signIn(
|
|
||||||
credentials.username, credentials.password);
|
|
||||||
if (signedIn) {
|
|
||||||
await routeState.go('/books/popular');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
// Display the app
|
|
||||||
FadeTransitionPage<void>(
|
|
||||||
key: _scaffoldKey,
|
|
||||||
child: const BookstoreScaffold(),
|
|
||||||
),
|
|
||||||
// Add an additional page to the stack if the user is viewing a book
|
|
||||||
// or an author
|
|
||||||
if (selectedBook != null)
|
|
||||||
MaterialPage<void>(
|
|
||||||
key: _bookDetailsKey,
|
|
||||||
child: BookDetailsScreen(
|
|
||||||
book: selectedBook,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (selectedAuthor != null)
|
|
||||||
MaterialPage<void>(
|
|
||||||
key: _authorDetailsKey,
|
|
||||||
child: AuthorDetailsScreen(
|
|
||||||
author: selectedAuthor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
|
||||||
// for details. All rights reserved. Use of this source code is governed by a
|
|
||||||
// BSD-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../routing.dart';
|
|
||||||
import '../screens/settings.dart';
|
|
||||||
import '../widgets/fade_transition_page.dart';
|
|
||||||
import 'authors.dart';
|
|
||||||
import 'books.dart';
|
|
||||||
import 'scaffold.dart';
|
|
||||||
|
|
||||||
/// Displays the contents of the body of [BookstoreScaffold]
|
|
||||||
class BookstoreScaffoldBody extends StatelessWidget {
|
|
||||||
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
|
||||||
|
|
||||||
const BookstoreScaffoldBody({
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var currentRoute = RouteStateScope.of(context).route;
|
|
||||||
|
|
||||||
// A nested Router isn't necessary because the back button behavior doesn't
|
|
||||||
// need to be customized.
|
|
||||||
return Navigator(
|
|
||||||
key: navigatorKey,
|
|
||||||
onPopPage: (route, dynamic result) => route.didPop(result),
|
|
||||||
pages: [
|
|
||||||
if (currentRoute.pathTemplate.startsWith('/authors'))
|
|
||||||
const FadeTransitionPage<void>(
|
|
||||||
key: ValueKey('authors'),
|
|
||||||
child: AuthorsScreen(),
|
|
||||||
)
|
|
||||||
else if (currentRoute.pathTemplate.startsWith('/settings'))
|
|
||||||
const FadeTransitionPage<void>(
|
|
||||||
key: ValueKey('settings'),
|
|
||||||
child: SettingsScreen(),
|
|
||||||
)
|
|
||||||
else if (currentRoute.pathTemplate.startsWith('/books') ||
|
|
||||||
currentRoute.pathTemplate == '/')
|
|
||||||
const FadeTransitionPage<void>(
|
|
||||||
key: ValueKey('books'),
|
|
||||||
child: BooksScreen(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Avoid building a Navigator with an empty `pages` list when the
|
|
||||||
// RouteState is set to an unexpected path, such as /signin.
|
|
||||||
//
|
|
||||||
// Since RouteStateScope is an InheritedNotifier, any change to the
|
|
||||||
// route will result in a call to this build method, even though this
|
|
||||||
// widget isn't built when those routes are active.
|
|
||||||
else
|
|
||||||
FadeTransitionPage<void>(
|
|
||||||
key: const ValueKey('empty'),
|
|
||||||
child: Container(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in new issue