mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
Update Navigation and Routing sample (#851)
* Add duration parameter to FadeTransitionPage * Use didChangeDependencies instead of didUpdateWidget * Don't notify listeners if the path hasn't changed * Update navigation sample WIP * Use Link and RouteStateScope in settings screen * update README * use named parameters for Library.addBook() * Make _handleAuthStateChanged synchronous * add missing copyright headers * Address code review comments * Address code review comments
This commit is contained in:
@@ -10,6 +10,14 @@ import 'src/app.dart';
|
||||
void main() {
|
||||
// Use package:url_strategy until this pull request is released:
|
||||
// https://github.com/flutter/flutter/pull/77103
|
||||
setPathUrlStrategy();
|
||||
|
||||
// Use to setHashUrlStrategy() to use "/#/" in the address bar (default). Use
|
||||
// setPathUrlStrategy() to use the path. You may need to configure your web
|
||||
// server to redirect all paths to index.html.
|
||||
//
|
||||
// On mobile platforms, both functions are no-ops.
|
||||
setHashUrlStrategy();
|
||||
// setPathUrlStrategy();
|
||||
|
||||
runApp(const Bookstore());
|
||||
}
|
||||
|
||||
@@ -26,10 +26,26 @@ class _BookstoreState extends State<Bookstore> {
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
final library = Library()
|
||||
..addBook('Left Hand of Darkness', 'Ursula K. Le Guin', true, true)
|
||||
..addBook('Too Like the Lightning', 'Ada Palmer', false, true)
|
||||
..addBook('Kindred', 'Octavia E. Butler', true, false)
|
||||
..addBook('The Lathe of Heaven', 'Ursula K. Le Guin', false, false);
|
||||
..addBook(
|
||||
title: 'Left Hand of Darkness',
|
||||
authorName: 'Ursula K. Le Guin',
|
||||
isPopular: true,
|
||||
isNew: true)
|
||||
..addBook(
|
||||
title: 'Too Like the Lightning',
|
||||
authorName: 'Ada Palmer',
|
||||
isPopular: false,
|
||||
isNew: true)
|
||||
..addBook(
|
||||
title: 'Kindred',
|
||||
authorName: 'Octavia E. Butler',
|
||||
isPopular: true,
|
||||
isNew: false)
|
||||
..addBook(
|
||||
title: 'The Lathe of Heaven',
|
||||
authorName: 'Ursula K. Le Guin',
|
||||
isPopular: false,
|
||||
isNew: false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -37,7 +53,7 @@ class _BookstoreState extends State<Bookstore> {
|
||||
|
||||
/// Configure the parser with all of the app's allowed path templates.
|
||||
routeParser = TemplateRouteParser(
|
||||
[
|
||||
allowedPaths: [
|
||||
'/signin',
|
||||
'/authors',
|
||||
'/settings',
|
||||
@@ -58,7 +74,6 @@ class _BookstoreState extends State<Bookstore> {
|
||||
navigatorKey: navigatorKey,
|
||||
builder: (context) => BookstoreNavigator(
|
||||
navigatorKey: navigatorKey,
|
||||
auth: auth,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -85,7 +100,7 @@ class _BookstoreState extends State<Bookstore> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleAuthStateChanged() async {
|
||||
void _handleAuthStateChanged() {
|
||||
if (!auth.signedIn) {
|
||||
routeState.go('/signin');
|
||||
}
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
// 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 'auth/auth.dart';
|
||||
export 'auth/auth_guard.dart';
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// 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 'data/author.dart';
|
||||
export 'data/book.dart';
|
||||
export 'data/library.dart';
|
||||
|
||||
@@ -11,7 +11,12 @@ class Library {
|
||||
final List<Book> allBooks = [];
|
||||
final List<Author> allAuthors = [];
|
||||
|
||||
void addBook(String title, String authorName, bool isPopular, bool isNew) {
|
||||
void addBook({
|
||||
required String title,
|
||||
required String authorName,
|
||||
required bool isPopular,
|
||||
required bool isNew,
|
||||
}) {
|
||||
var author =
|
||||
allAuthors.firstWhereOrNull((author) => author.name == authorName);
|
||||
var book = Book(allBooks.length, title, isPopular, isNew);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// 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';
|
||||
|
||||
@@ -9,9 +9,16 @@ 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>();
|
||||
|
||||
@@ -7,6 +7,11 @@ import 'package:path_to_regexp/path_to_regexp.dart';
|
||||
|
||||
import 'parsed_route.dart';
|
||||
|
||||
/// Used by [TemplateRouteParser] to guard access to routes.
|
||||
///
|
||||
/// Override this class to change the route that is returned by
|
||||
/// [TemplateRouteParser.parseRouteInformation] if a condition is not met, for
|
||||
/// example, if the user is not signed in.
|
||||
abstract class RouteGuard<T> {
|
||||
Future<T> redirect(T from);
|
||||
}
|
||||
@@ -17,11 +22,16 @@ class TemplateRouteParser extends RouteInformationParser<ParsedRoute> {
|
||||
RouteGuard<ParsedRoute>? guard;
|
||||
final ParsedRoute initialRoute;
|
||||
|
||||
TemplateRouteParser(List<String> pathTemplates,
|
||||
{String? initialRoute = '/', this.guard})
|
||||
: 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 ?? '/', {}, {}) {
|
||||
for (var template in pathTemplates) {
|
||||
for (var template in allowedPaths) {
|
||||
_addRoute(template);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ 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
|
||||
/// `RouteState.of(context)` and call `go()`:
|
||||
/// The current route state. To change the current route, call obtain the state
|
||||
/// using `RouteStateScope.of(context)` and call `go()`:
|
||||
///
|
||||
/// ```
|
||||
/// RouteState.of(context).go('/book/2');
|
||||
/// RouteStateScope.of(context).go('/book/2');
|
||||
/// ```
|
||||
class RouteState extends ChangeNotifier {
|
||||
TemplateRouteParser parser;
|
||||
@@ -24,6 +24,9 @@ class RouteState extends ChangeNotifier {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class BookDetailsScreen extends StatelessWidget {
|
||||
if (book == null) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('No book with found.'),
|
||||
child: Text('No book found.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,20 @@ class _BooksScreenState extends State<BooksScreen>
|
||||
..addListener(_handleTabIndexChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
final newPath = routeState.route.pathTemplate;
|
||||
if (newPath.startsWith('/books/popular')) {
|
||||
_tabController.index = 0;
|
||||
} else if (newPath.startsWith('/books/new')) {
|
||||
_tabController.index = 1;
|
||||
} else if (newPath == '/books/all') {
|
||||
_tabController.index = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.removeListener(_handleTabIndexChanged);
|
||||
@@ -114,17 +128,4 @@ class _BooksScreenState extends State<BooksScreen>
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(BooksScreen oldWidget) {
|
||||
var newPath = routeState.route.pathTemplate;
|
||||
if (newPath.startsWith('/books/popular')) {
|
||||
_tabController.index = 0;
|
||||
} else if (newPath.startsWith('/books/new')) {
|
||||
_tabController.index = 1;
|
||||
} else if (newPath == '/books/all') {
|
||||
_tabController.index = 2;
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// 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';
|
||||
|
||||
@@ -15,10 +19,8 @@ import 'scaffold.dart';
|
||||
/// on the [routeState] that was parsed by the TemplateRouteParser.
|
||||
class BookstoreNavigator extends StatefulWidget {
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
final BookstoreAuth auth;
|
||||
|
||||
const BookstoreNavigator({
|
||||
required this.auth,
|
||||
required this.navigatorKey,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
@@ -28,6 +30,7 @@ class BookstoreNavigator extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
||||
final signInKey = const ValueKey('Sign in');
|
||||
final scaffoldKey = const ValueKey<String>('App scaffold');
|
||||
final bookDetailsKey = const ValueKey<String>('Book details screen');
|
||||
final authorDetailsKey = const ValueKey<String>('Author details screen');
|
||||
@@ -35,18 +38,19 @@ class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final routeState = RouteStateScope.of(context)!;
|
||||
final authState = BookstoreAuthScope.of(context)!;
|
||||
final pathTemplate = routeState.route.pathTemplate;
|
||||
final library = LibraryScope.of(context);
|
||||
|
||||
Book? book;
|
||||
Book? selectedBook;
|
||||
if (pathTemplate == '/book/:bookId') {
|
||||
book = library.allBooks.firstWhereOrNull(
|
||||
selectedBook = library.allBooks.firstWhereOrNull(
|
||||
(b) => b.id.toString() == routeState.route.parameters['bookId']);
|
||||
}
|
||||
|
||||
Author? author;
|
||||
Author? selectedAuthor;
|
||||
if (pathTemplate == '/author/:authorId') {
|
||||
author = library.allAuthors.firstWhereOrNull(
|
||||
selectedAuthor = library.allAuthors.firstWhereOrNull(
|
||||
(b) => b.id.toString() == routeState.route.parameters['authorId']);
|
||||
}
|
||||
|
||||
@@ -71,11 +75,11 @@ class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
||||
if (routeState.route.pathTemplate == '/signin')
|
||||
// Display the sign in screen.
|
||||
FadeTransitionPage<void>(
|
||||
key: const ValueKey('Sign in'),
|
||||
key: signInKey,
|
||||
child: SignInScreen(
|
||||
onSignIn: (credentials) async {
|
||||
var signedIn = await widget.auth
|
||||
.signIn(credentials.username, credentials.password);
|
||||
var signedIn = await authState.signIn(
|
||||
credentials.username, credentials.password);
|
||||
if (signedIn) {
|
||||
routeState.go('/books/popular');
|
||||
}
|
||||
@@ -90,18 +94,18 @@ class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
||||
),
|
||||
// Add an additional page to the stack if the user is viewing a book
|
||||
// or an author
|
||||
if (book != null)
|
||||
if (selectedBook != null)
|
||||
MaterialPage<void>(
|
||||
key: bookDetailsKey,
|
||||
child: BookDetailsScreen(
|
||||
book: book,
|
||||
book: selectedBook,
|
||||
),
|
||||
)
|
||||
else if (author != null)
|
||||
else if (selectedAuthor != null)
|
||||
MaterialPage<void>(
|
||||
key: authorDetailsKey,
|
||||
child: AuthorDetailsScreen(
|
||||
author: author,
|
||||
author: selectedAuthor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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:bookstore/src/routing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/link.dart';
|
||||
|
||||
@@ -62,18 +63,15 @@ class SettingsContent extends StatelessWidget {
|
||||
uri: Uri.parse('/book/0'),
|
||||
builder: (context, followLink) {
|
||||
return TextButton(
|
||||
child: const Text('Go directly to /book/0'),
|
||||
child: const Text('Go directly to /book/0 (Link)'),
|
||||
onPressed: followLink,
|
||||
);
|
||||
},
|
||||
),
|
||||
Link(
|
||||
uri: Uri.parse('/author/0'),
|
||||
builder: (context, followLink) {
|
||||
return TextButton(
|
||||
child: const Text('Go directly to /author/0'),
|
||||
onPressed: followLink,
|
||||
);
|
||||
TextButton(
|
||||
child: const Text('Go directly to /book/0 (RouteState)'),
|
||||
onPressed: () {
|
||||
RouteStateScope.of(context)!.go('/book/0');
|
||||
},
|
||||
),
|
||||
].map((w) => Padding(padding: const EdgeInsets.all(8), child: w)),
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
||||
class Credentials {
|
||||
final String username;
|
||||
final String password;
|
||||
|
||||
Credentials(this.username, this.password);
|
||||
}
|
||||
|
||||
@@ -24,8 +25,8 @@ class SignInScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SignInScreenState extends State<SignInScreen> {
|
||||
String username = '';
|
||||
String password = '';
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -42,26 +43,20 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
Text('Sign in', style: Theme.of(context).textTheme.headline4),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: 'Username'),
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
username = v;
|
||||
});
|
||||
},
|
||||
controller: _usernameController,
|
||||
),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
obscureText: true,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
password = v;
|
||||
});
|
||||
},
|
||||
controller: _passwordController,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
widget.onSignIn(Credentials(username, password));
|
||||
widget.onSignIn(Credentials(
|
||||
_usernameController.value.text,
|
||||
_passwordController.value.text));
|
||||
},
|
||||
child: const Text('Sign in'),
|
||||
),
|
||||
|
||||
@@ -6,9 +6,13 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class FadeTransitionPage<T> extends Page<T> {
|
||||
final Widget child;
|
||||
final Duration duration;
|
||||
|
||||
const FadeTransitionPage({LocalKey? key, required this.child})
|
||||
: super(key: key);
|
||||
const FadeTransitionPage({
|
||||
LocalKey? key,
|
||||
required this.child,
|
||||
this.duration = const Duration(milliseconds: 300),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Route<T> createRoute(BuildContext context) {
|
||||
@@ -17,10 +21,9 @@ class FadeTransitionPage<T> extends Page<T> {
|
||||
}
|
||||
|
||||
class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
|
||||
PageBasedFadeTransitionRoute(Page page)
|
||||
: super(
|
||||
settings: page,
|
||||
);
|
||||
final FadeTransitionPage<T> page;
|
||||
|
||||
PageBasedFadeTransitionRoute(this.page) : super(settings: page);
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
@@ -29,7 +32,7 @@ class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||
Duration get transitionDuration => page.duration;
|
||||
|
||||
@override
|
||||
bool get maintainState => true;
|
||||
|
||||
Reference in New Issue
Block a user