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:
@@ -1,5 +1,148 @@
|
|||||||
# bookstore
|
# Navigation and Routing
|
||||||
|
A sample that shows how to use the [Router][] API to handle common navigation
|
||||||
|
scenarios.
|
||||||
|
|
||||||
This sample shows how to set up a Router using a custom RouterDelegate and
|
## Goals
|
||||||
RouteInformationParser.
|
- Demonstrate common navigation scenarios:
|
||||||
|
- Parsing path parameters ('/user/:id')
|
||||||
|
- Sign in (validation / guards)
|
||||||
|
- Nested navigation
|
||||||
|
- Provide a reusable implementation of RouterDelegate and RouteInformationParser
|
||||||
|
- Demonstrate how [deep linking][] is configured on iOS and Android
|
||||||
|
- Demonstrate how to use the Link widget from `package:url_Launcher` with the
|
||||||
|
Router API.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
The top-level widget, `Bookstore`, sets up the state for this app. It places
|
||||||
|
three `InheritedNotifier` widgets in the tree: `RouteStateScope`,
|
||||||
|
`BookstoreAuthScope`, and `LibraryScope`, which provide the state for the
|
||||||
|
application:
|
||||||
|
|
||||||
|
- **`RouteState`**: stores the current route path (`/book/1`) as a `ParsedRoute`
|
||||||
|
object (see below).
|
||||||
|
- **`BookstoreAuthScope`**: stores a mock authentication API, `BookstoreAuth`.
|
||||||
|
- **`LibraryScope`**: stores the data for the app, `Library`.
|
||||||
|
|
||||||
|
The `Bookstore` widget also uses the [MaterialApp.router()][router-ctor]
|
||||||
|
constructor to opt-in to the [Router][] API. This constructor requires a
|
||||||
|
[RouterDelegate][] and [RouteInformationParser][]. This app uses the
|
||||||
|
`routing.dart` library, described below.
|
||||||
|
|
||||||
|
## routing.dart
|
||||||
|
This library contains a general-purpose routing solution for medium-sized apps.
|
||||||
|
It implements these classes:
|
||||||
|
|
||||||
|
- **`SimpleRouterDelegate`**: Implements `RouterDelegate`. Updates `RouteState` when
|
||||||
|
a new route has been pushed to the application by the operating system. Also
|
||||||
|
notifies the `Router` widget whenever the `RouteState` changes.
|
||||||
|
- **`TemplateRouteParser`**: Implements RouteInformationParser. Parses the
|
||||||
|
incoming route path into a `ParsedRoute` object. A `RouteGuard` can be
|
||||||
|
provided to guard access to certain routes.
|
||||||
|
- **`ParsedRoute`**: Contains the current route location ("/user/2"), path
|
||||||
|
parameters ({id: 2}), query parameters ("?search=abc"), and path template
|
||||||
|
("/user/:id")
|
||||||
|
- **`RouteState`**: Stores the current `ParsedRoute`.
|
||||||
|
- **`RouteGuard`**: Guards access to routes. Can be overridden to redirect the
|
||||||
|
incoming route if a condition isn't met.
|
||||||
|
|
||||||
|
## App Structure
|
||||||
|
|
||||||
|
The `SimpleRouterDelegate` constructor requires a `WidgetBuilder` parameter and
|
||||||
|
a `navigatorKey`. This app uses a `BookstoreNavigator` widget, which configures
|
||||||
|
a `Navigator` with a list of pages, based on the current `RouteState`.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
SimpleRouterDelegate(
|
||||||
|
routeState: routeState,
|
||||||
|
navigatorKey: navigatorKey,
|
||||||
|
builder: (context) => BookstoreNavigator(
|
||||||
|
navigatorKey: navigatorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
This `Navigator` is configured to display either the sign-in screen or the
|
||||||
|
`BookstoreScaffold`. An additional screen is stacked on top of the
|
||||||
|
`BookstoreScaffold` if a book or author is currently selected:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
return Navigator(
|
||||||
|
key: widget.navigatorKey,
|
||||||
|
onPopPage: (route, dynamic result) {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
pages: [
|
||||||
|
if (routeState.route.pathTemplate == '/signin')
|
||||||
|
FadeTransitionPage<void>(
|
||||||
|
key: signInKey,
|
||||||
|
child: SignInScreen(),
|
||||||
|
),
|
||||||
|
else ...[
|
||||||
|
FadeTransitionPage<void>(
|
||||||
|
key: scaffoldKey,
|
||||||
|
child: BookstoreScaffold(),
|
||||||
|
),
|
||||||
|
if (selectedBook != null)
|
||||||
|
MaterialPage<void>(
|
||||||
|
key: bookDetailsKey,
|
||||||
|
child: BookDetailsScreen(
|
||||||
|
book: selectedBook,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (selectedAuthor != null)
|
||||||
|
MaterialPage<void>(
|
||||||
|
key: authorDetailsKey,
|
||||||
|
child: AuthorDetailsScreen(
|
||||||
|
author: selectedAuthor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The `BookstoreScaffold` widget uses `package:adaptive_navigation` to build a
|
||||||
|
navigation rail or bottom navigation bar based on the size of the screen. The
|
||||||
|
body of this screen is `BookstoreScaffoldBody`, which configures a nested
|
||||||
|
Navigator to display either the `AuthorsScreen`, `SettingsScreen`, or
|
||||||
|
`BooksScreen` widget.
|
||||||
|
|
||||||
|
## Linking vs updating RouteState
|
||||||
|
|
||||||
|
There are two ways to change the current route, either by updating `RouteState`,
|
||||||
|
which the RouterDelegate listens to, or use the Link widget from
|
||||||
|
`package:url_launcher`. The `SettingsScreen` widget demonstrates both options:
|
||||||
|
|
||||||
|
```
|
||||||
|
Link(
|
||||||
|
uri: Uri.parse('/book/0'),
|
||||||
|
builder: (context, followLink) {
|
||||||
|
return TextButton(
|
||||||
|
child: const Text('Go directly to /book/0 (Link)'),
|
||||||
|
onPressed: followLink,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Go directly to /book/0 (RouteState)'),
|
||||||
|
onPressed: () {
|
||||||
|
RouteStateScope.of(context)!.go('/book/0');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
## Questions/issues
|
||||||
|
|
||||||
|
If you have a general question about the Router API, the best places to go are:
|
||||||
|
|
||||||
|
- [The FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev)
|
||||||
|
- [StackOverflow](https://stackoverflow.com/questions/tagged/flutter)
|
||||||
|
|
||||||
|
If you run into an issue with the sample itself, please file an issue
|
||||||
|
in the [main Flutter repo](https://github.com/flutter/flutter/issues).
|
||||||
|
|
||||||
|
[Router]: https://api.flutter.dev/flutter/widgets/Router-class.html
|
||||||
|
[RouterDelegate]: https://api.flutter.dev/flutter/widgets/RouterDelegate-class.html
|
||||||
|
[RouteInformationParser]: https://api.flutter.dev/flutter/widgets/RouteInformationParser-class.html
|
||||||
|
[router-ctor]: https://api.flutter.dev/flutter/material/MaterialApp/MaterialApp.router.html
|
||||||
|
[deep linking]: https://flutter.dev/docs/development/ui/navigation/deep-linking
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ import 'src/app.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
// Use package:url_strategy until this pull request is released:
|
// Use package:url_strategy until this pull request is released:
|
||||||
// https://github.com/flutter/flutter/pull/77103
|
// 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());
|
runApp(const Bookstore());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,26 @@ class _BookstoreState extends State<Bookstore> {
|
|||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
final library = Library()
|
final library = Library()
|
||||||
..addBook('Left Hand of Darkness', 'Ursula K. Le Guin', true, true)
|
..addBook(
|
||||||
..addBook('Too Like the Lightning', 'Ada Palmer', false, true)
|
title: 'Left Hand of Darkness',
|
||||||
..addBook('Kindred', 'Octavia E. Butler', true, false)
|
authorName: 'Ursula K. Le Guin',
|
||||||
..addBook('The Lathe of Heaven', 'Ursula K. Le Guin', false, false);
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -37,7 +53,7 @@ class _BookstoreState extends State<Bookstore> {
|
|||||||
|
|
||||||
/// Configure the parser with all of the app's allowed path templates.
|
/// Configure the parser with all of the app's allowed path templates.
|
||||||
routeParser = TemplateRouteParser(
|
routeParser = TemplateRouteParser(
|
||||||
[
|
allowedPaths: [
|
||||||
'/signin',
|
'/signin',
|
||||||
'/authors',
|
'/authors',
|
||||||
'/settings',
|
'/settings',
|
||||||
@@ -58,7 +74,6 @@ class _BookstoreState extends State<Bookstore> {
|
|||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
builder: (context) => BookstoreNavigator(
|
builder: (context) => BookstoreNavigator(
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
auth: auth,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,7 +100,7 @@ class _BookstoreState extends State<Bookstore> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleAuthStateChanged() async {
|
void _handleAuthStateChanged() {
|
||||||
if (!auth.signedIn) {
|
if (!auth.signedIn) {
|
||||||
routeState.go('/signin');
|
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.dart';
|
||||||
export 'auth/auth_guard.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/author.dart';
|
||||||
export 'data/book.dart';
|
export 'data/book.dart';
|
||||||
export 'data/library.dart';
|
export 'data/library.dart';
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ class Library {
|
|||||||
final List<Book> allBooks = [];
|
final List<Book> allBooks = [];
|
||||||
final List<Author> allAuthors = [];
|
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 =
|
var author =
|
||||||
allAuthors.firstWhereOrNull((author) => author.name == authorName);
|
allAuthors.firstWhereOrNull((author) => author.name == authorName);
|
||||||
var book = Book(allBooks.length, title, isPopular, isNew);
|
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/delegate.dart';
|
||||||
export 'routing/parsed_route.dart';
|
export 'routing/parsed_route.dart';
|
||||||
export 'routing/parser.dart';
|
export 'routing/parser.dart';
|
||||||
|
|||||||
@@ -9,9 +9,16 @@ import 'parser.dart';
|
|||||||
|
|
||||||
/// A route path that has been parsed by [TemplateRouteParser].
|
/// A route path that has been parsed by [TemplateRouteParser].
|
||||||
class ParsedRoute {
|
class ParsedRoute {
|
||||||
|
/// The current path location without query parameters. (/book/123)
|
||||||
final String path;
|
final String path;
|
||||||
|
|
||||||
|
/// The path template (/book/:id)
|
||||||
final String pathTemplate;
|
final String pathTemplate;
|
||||||
|
|
||||||
|
/// The path parameters ({id: 123})
|
||||||
final Map<String, String> parameters;
|
final Map<String, String> parameters;
|
||||||
|
|
||||||
|
/// The query parameters ({search: abc})
|
||||||
final Map<String, String> queryParameters;
|
final Map<String, String> queryParameters;
|
||||||
|
|
||||||
static const _mapEquality = MapEquality<String, String>();
|
static const _mapEquality = MapEquality<String, String>();
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import 'package:path_to_regexp/path_to_regexp.dart';
|
|||||||
|
|
||||||
import 'parsed_route.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> {
|
abstract class RouteGuard<T> {
|
||||||
Future<T> redirect(T from);
|
Future<T> redirect(T from);
|
||||||
}
|
}
|
||||||
@@ -17,11 +22,16 @@ class TemplateRouteParser extends RouteInformationParser<ParsedRoute> {
|
|||||||
RouteGuard<ParsedRoute>? guard;
|
RouteGuard<ParsedRoute>? guard;
|
||||||
final ParsedRoute initialRoute;
|
final ParsedRoute initialRoute;
|
||||||
|
|
||||||
TemplateRouteParser(List<String> pathTemplates,
|
TemplateRouteParser({
|
||||||
{String? initialRoute = '/', this.guard})
|
/// The list of allowed path templates (['/', '/users/:id'])
|
||||||
: initialRoute =
|
required List<String> allowedPaths,
|
||||||
|
/// The initial route
|
||||||
|
String? initialRoute = '/',
|
||||||
|
/// [RouteGuard] used to redirect.
|
||||||
|
this.guard,
|
||||||
|
}) : initialRoute =
|
||||||
ParsedRoute(initialRoute ?? '/', initialRoute ?? '/', {}, {}) {
|
ParsedRoute(initialRoute ?? '/', initialRoute ?? '/', {}, {}) {
|
||||||
for (var template in pathTemplates) {
|
for (var template in allowedPaths) {
|
||||||
_addRoute(template);
|
_addRoute(template);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import 'package:flutter/widgets.dart';
|
|||||||
import 'parsed_route.dart';
|
import 'parsed_route.dart';
|
||||||
import 'parser.dart';
|
import 'parser.dart';
|
||||||
|
|
||||||
/// The current route state. To change the current route, call obtain the state using
|
/// The current route state. To change the current route, call obtain the state
|
||||||
/// `RouteState.of(context)` and call `go()`:
|
/// using `RouteStateScope.of(context)` and call `go()`:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// RouteState.of(context).go('/book/2');
|
/// RouteStateScope.of(context).go('/book/2');
|
||||||
/// ```
|
/// ```
|
||||||
class RouteState extends ChangeNotifier {
|
class RouteState extends ChangeNotifier {
|
||||||
TemplateRouteParser parser;
|
TemplateRouteParser parser;
|
||||||
@@ -24,6 +24,9 @@ class RouteState extends ChangeNotifier {
|
|||||||
ParsedRoute get route => _route;
|
ParsedRoute get route => _route;
|
||||||
|
|
||||||
set route(ParsedRoute route) {
|
set route(ParsedRoute route) {
|
||||||
|
// Don't notify listeners if the path hasn't changed.
|
||||||
|
if (_route == route) return;
|
||||||
|
|
||||||
_route = route;
|
_route = route;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class BookDetailsScreen extends StatelessWidget {
|
|||||||
if (book == null) {
|
if (book == null) {
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text('No book with found.'),
|
child: Text('No book found.'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ class _BooksScreenState extends State<BooksScreen>
|
|||||||
..addListener(_handleTabIndexChanged);
|
..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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tabController.removeListener(_handleTabIndexChanged);
|
_tabController.removeListener(_handleTabIndexChanged);
|
||||||
@@ -114,17 +128,4 @@ class _BooksScreenState extends State<BooksScreen>
|
|||||||
break;
|
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:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@@ -15,10 +19,8 @@ import 'scaffold.dart';
|
|||||||
/// on the [routeState] that was parsed by the TemplateRouteParser.
|
/// on the [routeState] that was parsed by the TemplateRouteParser.
|
||||||
class BookstoreNavigator extends StatefulWidget {
|
class BookstoreNavigator extends StatefulWidget {
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
final GlobalKey<NavigatorState> navigatorKey;
|
||||||
final BookstoreAuth auth;
|
|
||||||
|
|
||||||
const BookstoreNavigator({
|
const BookstoreNavigator({
|
||||||
required this.auth,
|
|
||||||
required this.navigatorKey,
|
required this.navigatorKey,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@@ -28,6 +30,7 @@ class BookstoreNavigator extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
||||||
|
final signInKey = const ValueKey('Sign in');
|
||||||
final scaffoldKey = const ValueKey<String>('App scaffold');
|
final scaffoldKey = const ValueKey<String>('App scaffold');
|
||||||
final bookDetailsKey = const ValueKey<String>('Book details screen');
|
final bookDetailsKey = const ValueKey<String>('Book details screen');
|
||||||
final authorDetailsKey = const ValueKey<String>('Author details screen');
|
final authorDetailsKey = const ValueKey<String>('Author details screen');
|
||||||
@@ -35,18 +38,19 @@ class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final routeState = RouteStateScope.of(context)!;
|
final routeState = RouteStateScope.of(context)!;
|
||||||
|
final authState = BookstoreAuthScope.of(context)!;
|
||||||
final pathTemplate = routeState.route.pathTemplate;
|
final pathTemplate = routeState.route.pathTemplate;
|
||||||
final library = LibraryScope.of(context);
|
final library = LibraryScope.of(context);
|
||||||
|
|
||||||
Book? book;
|
Book? selectedBook;
|
||||||
if (pathTemplate == '/book/:bookId') {
|
if (pathTemplate == '/book/:bookId') {
|
||||||
book = library.allBooks.firstWhereOrNull(
|
selectedBook = library.allBooks.firstWhereOrNull(
|
||||||
(b) => b.id.toString() == routeState.route.parameters['bookId']);
|
(b) => b.id.toString() == routeState.route.parameters['bookId']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Author? author;
|
Author? selectedAuthor;
|
||||||
if (pathTemplate == '/author/:authorId') {
|
if (pathTemplate == '/author/:authorId') {
|
||||||
author = library.allAuthors.firstWhereOrNull(
|
selectedAuthor = library.allAuthors.firstWhereOrNull(
|
||||||
(b) => b.id.toString() == routeState.route.parameters['authorId']);
|
(b) => b.id.toString() == routeState.route.parameters['authorId']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,11 +75,11 @@ class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
|||||||
if (routeState.route.pathTemplate == '/signin')
|
if (routeState.route.pathTemplate == '/signin')
|
||||||
// Display the sign in screen.
|
// Display the sign in screen.
|
||||||
FadeTransitionPage<void>(
|
FadeTransitionPage<void>(
|
||||||
key: const ValueKey('Sign in'),
|
key: signInKey,
|
||||||
child: SignInScreen(
|
child: SignInScreen(
|
||||||
onSignIn: (credentials) async {
|
onSignIn: (credentials) async {
|
||||||
var signedIn = await widget.auth
|
var signedIn = await authState.signIn(
|
||||||
.signIn(credentials.username, credentials.password);
|
credentials.username, credentials.password);
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
routeState.go('/books/popular');
|
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
|
// Add an additional page to the stack if the user is viewing a book
|
||||||
// or an author
|
// or an author
|
||||||
if (book != null)
|
if (selectedBook != null)
|
||||||
MaterialPage<void>(
|
MaterialPage<void>(
|
||||||
key: bookDetailsKey,
|
key: bookDetailsKey,
|
||||||
child: BookDetailsScreen(
|
child: BookDetailsScreen(
|
||||||
book: book,
|
book: selectedBook,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (author != null)
|
else if (selectedAuthor != null)
|
||||||
MaterialPage<void>(
|
MaterialPage<void>(
|
||||||
key: authorDetailsKey,
|
key: authorDetailsKey,
|
||||||
child: AuthorDetailsScreen(
|
child: AuthorDetailsScreen(
|
||||||
author: author,
|
author: selectedAuthor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// for details. All rights reserved. Use of this source code is governed by a
|
// 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.
|
// BSD-style license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:bookstore/src/routing.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:url_launcher/link.dart';
|
import 'package:url_launcher/link.dart';
|
||||||
|
|
||||||
@@ -62,18 +63,15 @@ class SettingsContent extends StatelessWidget {
|
|||||||
uri: Uri.parse('/book/0'),
|
uri: Uri.parse('/book/0'),
|
||||||
builder: (context, followLink) {
|
builder: (context, followLink) {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
child: const Text('Go directly to /book/0'),
|
child: const Text('Go directly to /book/0 (Link)'),
|
||||||
onPressed: followLink,
|
onPressed: followLink,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Link(
|
TextButton(
|
||||||
uri: Uri.parse('/author/0'),
|
child: const Text('Go directly to /book/0 (RouteState)'),
|
||||||
builder: (context, followLink) {
|
onPressed: () {
|
||||||
return TextButton(
|
RouteStateScope.of(context)!.go('/book/0');
|
||||||
child: const Text('Go directly to /author/0'),
|
|
||||||
onPressed: followLink,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
].map((w) => Padding(padding: const EdgeInsets.all(8), child: w)),
|
].map((w) => Padding(padding: const EdgeInsets.all(8), child: w)),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
class Credentials {
|
class Credentials {
|
||||||
final String username;
|
final String username;
|
||||||
final String password;
|
final String password;
|
||||||
|
|
||||||
Credentials(this.username, this.password);
|
Credentials(this.username, this.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,8 +25,8 @@ class SignInScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SignInScreenState extends State<SignInScreen> {
|
class _SignInScreenState extends State<SignInScreen> {
|
||||||
String username = '';
|
final _usernameController = TextEditingController();
|
||||||
String password = '';
|
final _passwordController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -42,26 +43,20 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
Text('Sign in', style: Theme.of(context).textTheme.headline4),
|
Text('Sign in', style: Theme.of(context).textTheme.headline4),
|
||||||
TextField(
|
TextField(
|
||||||
decoration: const InputDecoration(labelText: 'Username'),
|
decoration: const InputDecoration(labelText: 'Username'),
|
||||||
onChanged: (v) {
|
controller: _usernameController,
|
||||||
setState(() {
|
|
||||||
username = v;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
TextField(
|
TextField(
|
||||||
decoration: const InputDecoration(labelText: 'Password'),
|
decoration: const InputDecoration(labelText: 'Password'),
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
onChanged: (v) {
|
controller: _passwordController,
|
||||||
setState(() {
|
|
||||||
password = v;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
widget.onSignIn(Credentials(username, password));
|
widget.onSignIn(Credentials(
|
||||||
|
_usernameController.value.text,
|
||||||
|
_passwordController.value.text));
|
||||||
},
|
},
|
||||||
child: const Text('Sign in'),
|
child: const Text('Sign in'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class FadeTransitionPage<T> extends Page<T> {
|
class FadeTransitionPage<T> extends Page<T> {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
const FadeTransitionPage({LocalKey? key, required this.child})
|
const FadeTransitionPage({
|
||||||
: super(key: key);
|
LocalKey? key,
|
||||||
|
required this.child,
|
||||||
|
this.duration = const Duration(milliseconds: 300),
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Route<T> createRoute(BuildContext context) {
|
Route<T> createRoute(BuildContext context) {
|
||||||
@@ -17,10 +21,9 @@ class FadeTransitionPage<T> extends Page<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
|
class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
|
||||||
PageBasedFadeTransitionRoute(Page page)
|
final FadeTransitionPage<T> page;
|
||||||
: super(
|
|
||||||
settings: page,
|
PageBasedFadeTransitionRoute(this.page) : super(settings: page);
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color? get barrierColor => null;
|
Color? get barrierColor => null;
|
||||||
@@ -29,7 +32,7 @@ class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
|
|||||||
String? get barrierLabel => null;
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Duration get transitionDuration => const Duration(milliseconds: 300);
|
Duration get transitionDuration => page.duration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get maintainState => true;
|
bool get maintainState => true;
|
||||||
|
|||||||
@@ -9,10 +9,26 @@ void main() {
|
|||||||
group('Library', () {
|
group('Library', () {
|
||||||
test('addBook', () {
|
test('addBook', () {
|
||||||
final library = Library();
|
final library = Library();
|
||||||
library.addBook('Left Hand of Darkness', 'Ursula K. Le Guin', true, true);
|
library.addBook(
|
||||||
library.addBook('Too Like the Lightning', 'Ada Palmer', false, true);
|
title: 'Left Hand of Darkness',
|
||||||
library.addBook('Kindred', 'Octavia E. Butler', true, false);
|
authorName: 'Ursula K. Le Guin',
|
||||||
library.addBook('The Lathe of Heaven', 'Ursula K. Le Guin', false, false);
|
isPopular: true,
|
||||||
|
isNew: true);
|
||||||
|
library.addBook(
|
||||||
|
title: 'Too Like the Lightning',
|
||||||
|
authorName: 'Ada Palmer',
|
||||||
|
isPopular: false,
|
||||||
|
isNew: true);
|
||||||
|
library.addBook(
|
||||||
|
title: 'Kindred',
|
||||||
|
authorName: 'Octavia E. Butler',
|
||||||
|
isPopular: true,
|
||||||
|
isNew: false);
|
||||||
|
library.addBook(
|
||||||
|
title: 'The Lathe of Heaven',
|
||||||
|
authorName: 'Ursula K. Le Guin',
|
||||||
|
isPopular: false,
|
||||||
|
isNew: false);
|
||||||
expect(library.allAuthors.length, 3);
|
expect(library.allAuthors.length, 3);
|
||||||
expect(library.allAuthors.first.books.length, 2);
|
expect(library.allAuthors.first.books.length, 2);
|
||||||
expect(library.allBooks.length, 4);
|
expect(library.allBooks.length, 4);
|
||||||
|
|||||||
Reference in New Issue
Block a user