1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-08 22:09:06 +00:00

Replace navigation_and_routing with a new sample (#832)

* move snippets into old_snippets directory

* add new navigation_and_routing sample

* add copyright headers

* Apply #827 to old_snippets/ directory and upgrade them to null safety

* Code review comments

- Move Guard class into parser.dart
- Move usage of guards from Delegate to RouteInformationParser
- Rename delegate to SimpleRouterDelegate

* clean up imports

* refactor settings screen, fix bug

* avoid conflicting paths /books/new and /books/1 - rename to book/1

* dispose fields in _BookstoreState class

* remove /books path

This was causing problems

* add comment

* Change BookstoreAuthScope and BookstoreAuthScope to InheritedNotifier

* fix warnings

* Make the initial route configurable, set to '/signin'

* Enable deep linking

https://flutter.dev/docs/development/ui/navigation/deep-linking

* use path URL strategy on the web.

* remove TODO, add comment
This commit is contained in:
John Ryan
2021-07-08 07:48:17 -07:00
committed by GitHub
parent 8573269c03
commit ae3c4e3c47
96 changed files with 3560 additions and 278 deletions

View File

@@ -0,0 +1,101 @@
// 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 'auth.dart';
import 'data.dart';
import 'routing.dart';
import 'screens/navigator.dart';
import 'widgets/library_scope.dart';
class Bookstore extends StatefulWidget {
const Bookstore({Key? key}) : super(key: key);
@override
_BookstoreState createState() => _BookstoreState();
}
class _BookstoreState extends State<Bookstore> {
final auth = BookstoreAuth();
late final BookstoreRouteGuard guard;
late final RouteState routeState;
late final SimpleRouterDelegate routerDelegate;
late final TemplateRouteParser routeParser;
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);
@override
void initState() {
guard = BookstoreRouteGuard(auth: auth);
/// Configure the parser with all of the app's allowed path templates.
routeParser = TemplateRouteParser(
[
'/signin',
'/authors',
'/settings',
'/books/new',
'/books/all',
'/books/popular',
'/book/:bookId',
'/author/:authorId',
],
guard: guard,
initialRoute: '/signin',
);
routeState = RouteState(routeParser);
routerDelegate = SimpleRouterDelegate(
routeState: routeState,
navigatorKey: navigatorKey,
builder: (context) => BookstoreNavigator(
navigatorKey: navigatorKey,
auth: auth,
),
);
// Listen for when the user logs out and display the signin screen.
auth.addListener(_handleAuthStateChanged);
super.initState();
}
@override
Widget build(BuildContext context) {
return RouteStateScope(
notifier: routeState,
child: BookstoreAuthScope(
notifier: auth,
child: LibraryScope(
library: library,
child: MaterialApp.router(
routerDelegate: routerDelegate,
routeInformationParser: routeParser,
),
),
),
);
}
Future<void> _handleAuthStateChanged() async {
if (!auth.signedIn) {
routeState.go('/signin');
}
}
@override
void dispose() {
auth.removeListener(_handleAuthStateChanged);
routeState.dispose();
routerDelegate.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,2 @@
export 'auth/auth.dart';
export 'auth/auth_guard.dart';

View File

@@ -0,0 +1,53 @@
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
/// A mock authentication service
class BookstoreAuth extends ChangeNotifier {
bool _signedIn = false;
bool get signedIn {
return _signedIn;
}
Future<void> signOut() async {
await Future<void>.delayed(const Duration(milliseconds: 200));
// Sign out.
_signedIn = false;
notifyListeners();
}
Future<bool> signIn(String username, String password) async {
await Future<void>.delayed(const Duration(milliseconds: 200));
// Sign in. Allow any password.
_signedIn = true;
notifyListeners();
return _signedIn;
}
@override
bool operator ==(Object other) {
return other is BookstoreAuth && other._signedIn == _signedIn;
}
@override
int get hashCode => _signedIn.hashCode;
}
class BookstoreAuthScope extends InheritedNotifier<BookstoreAuth> {
const BookstoreAuthScope({
required BookstoreAuth notifier,
required Widget child,
Key? key,
}) : super(key: key, notifier: notifier, child: child);
static BookstoreAuth? of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<BookstoreAuthScope>()
?.notifier;
}
}

View File

@@ -0,0 +1,32 @@
// 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 '../routing.dart';
import 'auth.dart';
/// An implementation of [RouteGuard] that redirects to /signIn
class BookstoreRouteGuard implements RouteGuard<ParsedRoute> {
BookstoreAuth auth;
BookstoreRouteGuard({
required this.auth,
});
/// Redirect to /signin if the user isn't signed in.
@override
Future<ParsedRoute> redirect(ParsedRoute from) async {
final signedIn = auth.signedIn;
final signInRoute = ParsedRoute('/signin', '/signin', {}, {});
// Go to /signin if the user is not signed in
if (!signedIn && from != signInRoute) {
return signInRoute;
}
// Go to /books if the user is signed in and tries to go to /signin.
else if (signedIn && from == signInRoute) {
return ParsedRoute('/books/popular', '/books/popular', {}, {});
}
return from;
}
}

View File

@@ -0,0 +1,3 @@
export 'data/author.dart';
export 'data/book.dart';
export 'data/library.dart';

View File

@@ -0,0 +1,13 @@
// 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 'book.dart';
class Author {
final int id;
final String name;
final List<Book> books;
Author(this.id, this.name, this.books);
}

View File

@@ -0,0 +1,15 @@
// 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 'author.dart';
class Book {
final int id;
final String title;
late final Author author;
final bool isPopular;
final bool isNew;
Book(this.id, this.title, this.isPopular, this.isNew);
}

View File

@@ -0,0 +1,41 @@
// 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 'author.dart';
import 'book.dart';
class Library {
final List<Book> allBooks = [];
final List<Author> allAuthors = [];
void addBook(String title, String authorName, bool isPopular, bool isNew) {
var author =
allAuthors.firstWhereOrNull((author) => author.name == authorName);
var book = Book(allBooks.length, title, isPopular, isNew);
if (author == null) {
author = Author(allAuthors.length, authorName, [book]);
allAuthors.add(author);
} else {
author.books.add(book);
}
book.author = author;
allBooks.add(book);
}
List<Book> get popularBooks {
return [
...allBooks.where((book) => book.isPopular),
];
}
List<Book> get newBooks {
return [
...allBooks.where((book) => book.isNew),
];
}
}

View File

@@ -0,0 +1,4 @@
export 'routing/delegate.dart';
export 'routing/parsed_route.dart';
export 'routing/parser.dart';
export 'routing/route_state.dart';

View File

@@ -0,0 +1,52 @@
// 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,
// ignore: prefer_initializing_formals
}) {
routeState.addListener(notifyListeners);
}
@override
Widget build(BuildContext context) {
return builder(context);
}
@override
Future<void> setNewRoutePath(ParsedRoute configuration) async {
routeState.route = configuration;
return SynchronousFuture(null);
}
@override
ParsedRoute get currentConfiguration {
return routeState.route;
}
@override
void dispose() {
routeState.removeListener(notifyListeners);
routeState.dispose();
super.dispose();
}
}

View File

@@ -0,0 +1,45 @@
// 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 {
final String path;
final String pathTemplate;
final Map<String, String> parameters;
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) {
return 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>';
}

View File

@@ -0,0 +1,68 @@
// 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';
abstract class RouteGuard<T> {
Future<T> redirect(T from);
}
/// Parses the URI path into a [ParsedRoute].
class TemplateRouteParser extends RouteInformationParser<ParsedRoute> {
final List<String> _pathTemplates = [];
RouteGuard<ParsedRoute>? guard;
final ParsedRoute initialRoute;
TemplateRouteParser(List<String> pathTemplates,
{String? initialRoute = '/', this.guard})
: initialRoute =
ParsedRoute(initialRoute ?? '/', initialRoute ?? '/', {}, {}) {
for (var template in pathTemplates) {
_addRoute(template);
}
}
void _addRoute(String pathTemplate) {
_pathTemplates.add(pathTemplate);
}
@override
Future<ParsedRoute> parseRouteInformation(
RouteInformation routeInformation) async {
return await _parse(routeInformation);
}
Future<ParsedRoute> _parse(RouteInformation routeInformation) async {
final path = routeInformation.location!;
final queryParams = Uri.parse(path).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.redirect(parsedRoute);
}
return parsedRoute;
}
@override
RouteInformation restoreRouteInformation(ParsedRoute configuration) {
return RouteInformation(location: configuration.path);
}
}

View File

@@ -0,0 +1,50 @@
// 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/foundation.dart';
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()`:
///
/// ```
/// RouteState.of(context).go('/book/2');
/// ```
class RouteState extends ChangeNotifier {
TemplateRouteParser parser;
ParsedRoute _route;
RouteState(this.parser)
: _route = parser.initialRoute;
ParsedRoute get route => _route;
set route(ParsedRoute route) {
_route = route;
notifyListeners();
}
Future<void> go(String route) async {
this.route =
await parser.parseRouteInformation(RouteInformation(location: route));
}
}
/// Provides the current [RouteState] to descendent widgets in the tree.
class RouteStateScope extends InheritedNotifier<RouteState> {
const RouteStateScope({
required RouteState notifier,
required Widget child,
Key? key,
}) : super(key: key, notifier: notifier, child: child);
static RouteState? of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<RouteStateScope>()
?.notifier;
}
}

View File

@@ -0,0 +1,49 @@
// 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 '../data.dart';
import '../widgets/book_list.dart';
import 'book_details.dart';
class AuthorDetailsScreen extends StatelessWidget {
final Author author;
const AuthorDetailsScreen({
Key? key,
required this.author,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(author.name),
),
body: Center(
child: Column(
children: [
Expanded(
child: BookList(
books: author.books,
onTap: (book) {
Navigator.of(context).push<dynamic>(
MaterialPageRoute<dynamic>(
builder: (context) {
return BookDetailsScreen(
book: book,
);
},
),
);
},
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,30 @@
// 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 '../widgets/author_list.dart';
import '../widgets/library_scope.dart';
class AuthorsScreen extends StatelessWidget {
final String title = "Authors";
const AuthorsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: AuthorList(
authors: LibraryScope.of(context).allAuthors,
onTap: (author) {
RouteStateScope.of(context)!.go('/author/${author.id}');
},
),
);
}
}

View File

@@ -0,0 +1,75 @@
// 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 'package:url_launcher/link.dart';
import '../data.dart';
import 'author_details.dart';
class BookDetailsScreen extends StatelessWidget {
final Book? book;
const BookDetailsScreen({
Key? key,
this.book,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (book == null) {
return const Scaffold(
body: Center(
child: Text('No book with found.'),
),
);
}
return Scaffold(
appBar: AppBar(
title: Text(book!.title),
),
body: Center(
child: Column(
children: [
Text(
book!.title,
style: Theme
.of(context)
.textTheme
.headline4,
),
Text(
book!.author.name,
style: Theme
.of(context)
.textTheme
.subtitle1,
),
TextButton(
child: const Text('View author (Push)'),
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (context) {
return AuthorDetailsScreen(author: book!.author);
},
),
);
},
),
Link(
uri: Uri.parse('/author/${book!.author.id}'),
builder: (context, followLink) {
return TextButton(
child: const Text('View author (Link)'),
onPressed: followLink,
);
},
),
],
),
),
);
}
}

View File

@@ -0,0 +1,130 @@
// 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 '../data.dart';
import '../routing.dart';
import '../widgets/book_list.dart';
import '../widgets/library_scope.dart';
class BooksScreen extends StatefulWidget {
final ParsedRoute currentRoute;
const BooksScreen({
Key? key,
required this.currentRoute,
}) : super(key: key);
@override
_BooksScreenState createState() => _BooksScreenState();
}
class _BooksScreenState extends State<BooksScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this)
..addListener(_handleTabIndexChanged);
}
@override
void dispose() {
_tabController.removeListener(_handleTabIndexChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
final library = LibraryScope.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Books'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(
text: 'Popular',
icon: Icon(Icons.people),
),
Tab(
text: 'New',
icon: Icon(Icons.new_releases),
),
Tab(
text: 'All',
icon: Icon(Icons.list),
),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
BookList(
books: library.popularBooks,
onTap: _handleBookTapped,
),
BookList(
books: library.newBooks,
onTap: _handleBookTapped,
),
BookList(
books: library.allBooks,
onTap: _handleBookTapped,
),
],
),
);
}
String get title {
switch (_tabController.index) {
case 1:
return 'New';
case 2:
return 'All';
case 0:
default:
return 'Popular';
}
}
RouteState get routeState => RouteStateScope.of(context)!;
void _handleBookTapped(Book book) {
routeState.go('/book/${book.id}');
}
void _handleTabIndexChanged() {
switch (_tabController.index) {
case 1:
routeState.go('/books/new');
break;
case 2:
routeState.go('/books/all');
break;
case 0:
default:
routeState.go('/books/popular');
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);
}
}

View File

@@ -0,0 +1,111 @@
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 '../widgets/library_scope.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;
final BookstoreAuth auth;
const BookstoreNavigator({
required this.auth,
required this.navigatorKey,
Key? key,
}) : super(key: key);
@override
_BookstoreNavigatorState createState() => _BookstoreNavigatorState();
}
class _BookstoreNavigatorState extends State<BookstoreNavigator> {
final scaffoldKey = const ValueKey<String>('App scaffold');
final bookDetailsKey = const ValueKey<String>('Book details screen');
final authorDetailsKey = const ValueKey<String>('Author details screen');
@override
Widget build(BuildContext context) {
final routeState = RouteStateScope.of(context)!;
final pathTemplate = routeState.route.pathTemplate;
final library = LibraryScope.of(context);
Book? book;
if (pathTemplate == '/book/:bookId') {
book = library.allBooks.firstWhereOrNull(
(b) => b.id.toString() == routeState.route.parameters['bookId']);
}
Author? author;
if (pathTemplate == '/author/:authorId') {
author = library.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: const ValueKey('Sign in'),
child: SignInScreen(
onSignIn: (credentials) async {
var signedIn = await widget.auth
.signIn(credentials.username, credentials.password);
if (signedIn) {
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 (book != null)
MaterialPage<void>(
key: bookDetailsKey,
child: BookDetailsScreen(
book: book,
),
)
else if (author != null)
MaterialPage<void>(
key: authorDetailsKey,
child: AuthorDetailsScreen(
author: author,
),
),
],
],
);
}
}

View File

@@ -0,0 +1,54 @@
// 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:adaptive_navigation/adaptive_navigation.dart';
import 'package:flutter/material.dart';
import '../routing.dart';
import 'scaffold_body.dart';
class BookstoreScaffold extends StatelessWidget {
const BookstoreScaffold({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final routeState = RouteStateScope.of(context)!;
final selectedIndex = _getSelectedIndex(routeState.route.pathTemplate);
return Scaffold(
body: AdaptiveNavigationScaffold(
selectedIndex: selectedIndex,
body: const BookstoreScaffoldBody(),
onDestinationSelected: (idx) {
if (idx == 0) routeState.go('/books/popular');
if (idx == 1) routeState.go('/authors');
if (idx == 2) routeState.go('/settings');
},
destinations: const [
AdaptiveScaffoldDestination(
title: 'Books',
icon: Icons.book,
),
AdaptiveScaffoldDestination(
title: 'Authors',
icon: Icons.person,
),
AdaptiveScaffoldDestination(
title: 'Settings',
icon: Icons.settings,
),
],
),
);
}
int _getSelectedIndex(String pathTemplate) {
if (pathTemplate.startsWith('/books')) return 0;
if (pathTemplate == '/authors') return 1;
if (pathTemplate == '/settings') return 2;
return 0;
}
}

View File

@@ -0,0 +1,62 @@
// 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';
/// Displays the contents of the body of [BookstoreScaffold]
class BookstoreScaffoldBody extends StatelessWidget {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
const BookstoreScaffoldBody({
Key? key,
}) : super(key: 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 == '/')
FadeTransitionPage<void>(
key: const ValueKey('books'),
child: BooksScreen(currentRoute: currentRoute),
)
// 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(),
),
],
);
}
}

View File

@@ -0,0 +1,103 @@
// 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 'package:url_launcher/link.dart';
import '../auth/auth.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({Key? key}) : super(key: key);
@override
_SettingsScreenState createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: const Card(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 12),
child: SettingsContent(),
),
),
),
),
),
),
);
}
}
class SettingsContent extends StatelessWidget {
const SettingsContent({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
...[
Text(
'Settings',
style: Theme.of(context).textTheme.headline4,
),
ElevatedButton(
onPressed: () {
BookstoreAuthScope.of(context)!.signOut();
},
child: const Text('Sign out'),
),
Link(
uri: Uri.parse('/book/0'),
builder: (context, followLink) {
return TextButton(
child: const Text('Go directly to /book/0'),
onPressed: followLink,
);
},
),
Link(
uri: Uri.parse('/author/0'),
builder: (context, followLink) {
return TextButton(
child: const Text('Go directly to /author/0'),
onPressed: followLink,
);
},
),
].map((w) => Padding(padding: const EdgeInsets.all(8), child: w)),
TextButton(
onPressed: () => showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Alert!'),
content: const Text('The alert description goes here.'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'Cancel'),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
),
child: const Text('Show Dialog'),
)
],
);
}
}

View File

@@ -0,0 +1,76 @@
// 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/cupertino.dart';
import 'package:flutter/material.dart';
class Credentials {
final String username;
final String password;
Credentials(this.username, this.password);
}
class SignInScreen extends StatefulWidget {
final ValueChanged<Credentials> onSignIn;
const SignInScreen({
required this.onSignIn,
Key? key,
}) : super(key: key);
@override
_SignInScreenState createState() => _SignInScreenState();
}
class _SignInScreenState extends State<SignInScreen> {
String username = '';
String password = '';
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Card(
child: Container(
constraints: BoxConstraints.loose(const Size(600, 600)),
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text('Sign in', style: Theme.of(context).textTheme.headline4),
TextField(
decoration: const InputDecoration(labelText: 'Username'),
onChanged: (v) {
setState(() {
username = v;
});
},
),
TextField(
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
onChanged: (v) {
setState(() {
password = v;
});
},
),
Padding(
padding: const EdgeInsets.all(16),
child: TextButton(
onPressed: () async {
widget.onSignIn(Credentials(username, password));
},
child: const Text('Sign in'),
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,36 @@
// 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 '../data.dart';
class AuthorList extends StatelessWidget {
final List<Author> authors;
final ValueChanged<Author>? onTap;
const AuthorList({
required this.authors,
this.onTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: authors.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
authors[index].name,
),
subtitle: Text(
'${authors[index].books.length} books',
),
onTap: onTap != null ? () => onTap!(authors[index]) : null,
);
},
);
}
}

View File

@@ -0,0 +1,36 @@
// 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 '../data.dart';
class BookList extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book>? onTap;
const BookList({
required this.books,
this.onTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: books.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
books[index].title,
),
subtitle: Text(
books[index].author.name,
),
onTap: onTap != null ? () => onTap!(books[index]) : null,
);
},
);
}
}

View File

@@ -0,0 +1,52 @@
// 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';
class FadeTransitionPage<T> extends Page<T> {
final Widget child;
const FadeTransitionPage({LocalKey? key, required this.child})
: super(key: key);
@override
Route<T> createRoute(BuildContext context) {
return PageBasedFadeTransitionRoute<T>(this);
}
}
class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
PageBasedFadeTransitionRoute(Page page)
: super(
settings: page,
);
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
@override
Duration get transitionDuration => const Duration(milliseconds: 300);
@override
bool get maintainState => true;
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
var curveTween = CurveTween(curve: Curves.easeIn);
return FadeTransition(
opacity: animation.drive(curveTween),
child: (settings as FadeTransitionPage).child,
);
}
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return child;
}
}

View File

@@ -0,0 +1,25 @@
// 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 '../data.dart';
class LibraryScope extends InheritedWidget {
final Library library;
const LibraryScope({
Key? key,
required this.library,
required Widget child,
}) : super(key: key, child: child);
@override
bool updateShouldNotify(LibraryScope oldWidget) =>
library != oldWidget.library;
static Library of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<LibraryScope>()!.library;
}
}