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:
101
navigation_and_routing/lib/src/app.dart
Normal file
101
navigation_and_routing/lib/src/app.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
2
navigation_and_routing/lib/src/auth.dart
Normal file
2
navigation_and_routing/lib/src/auth.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'auth/auth.dart';
|
||||
export 'auth/auth_guard.dart';
|
||||
53
navigation_and_routing/lib/src/auth/auth.dart
Normal file
53
navigation_and_routing/lib/src/auth/auth.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
32
navigation_and_routing/lib/src/auth/auth_guard.dart
Normal file
32
navigation_and_routing/lib/src/auth/auth_guard.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
3
navigation_and_routing/lib/src/data.dart
Normal file
3
navigation_and_routing/lib/src/data.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
export 'data/author.dart';
|
||||
export 'data/book.dart';
|
||||
export 'data/library.dart';
|
||||
13
navigation_and_routing/lib/src/data/author.dart
Normal file
13
navigation_and_routing/lib/src/data/author.dart
Normal 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);
|
||||
}
|
||||
15
navigation_and_routing/lib/src/data/book.dart
Normal file
15
navigation_and_routing/lib/src/data/book.dart
Normal 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);
|
||||
}
|
||||
41
navigation_and_routing/lib/src/data/library.dart
Normal file
41
navigation_and_routing/lib/src/data/library.dart
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
4
navigation_and_routing/lib/src/routing.dart
Normal file
4
navigation_and_routing/lib/src/routing.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
export 'routing/delegate.dart';
|
||||
export 'routing/parsed_route.dart';
|
||||
export 'routing/parser.dart';
|
||||
export 'routing/route_state.dart';
|
||||
52
navigation_and_routing/lib/src/routing/delegate.dart
Normal file
52
navigation_and_routing/lib/src/routing/delegate.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
45
navigation_and_routing/lib/src/routing/parsed_route.dart
Normal file
45
navigation_and_routing/lib/src/routing/parsed_route.dart
Normal 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>';
|
||||
}
|
||||
68
navigation_and_routing/lib/src/routing/parser.dart
Normal file
68
navigation_and_routing/lib/src/routing/parser.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
50
navigation_and_routing/lib/src/routing/route_state.dart
Normal file
50
navigation_and_routing/lib/src/routing/route_state.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
49
navigation_and_routing/lib/src/screens/author_details.dart
Normal file
49
navigation_and_routing/lib/src/screens/author_details.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
30
navigation_and_routing/lib/src/screens/authors.dart
Normal file
30
navigation_and_routing/lib/src/screens/authors.dart
Normal 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}');
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
navigation_and_routing/lib/src/screens/book_details.dart
Normal file
75
navigation_and_routing/lib/src/screens/book_details.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
130
navigation_and_routing/lib/src/screens/books.dart
Normal file
130
navigation_and_routing/lib/src/screens/books.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
111
navigation_and_routing/lib/src/screens/navigator.dart
Normal file
111
navigation_and_routing/lib/src/screens/navigator.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
54
navigation_and_routing/lib/src/screens/scaffold.dart
Normal file
54
navigation_and_routing/lib/src/screens/scaffold.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
62
navigation_and_routing/lib/src/screens/scaffold_body.dart
Normal file
62
navigation_and_routing/lib/src/screens/scaffold_body.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
103
navigation_and_routing/lib/src/screens/settings.dart
Normal file
103
navigation_and_routing/lib/src/screens/settings.dart
Normal 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'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
76
navigation_and_routing/lib/src/screens/sign_in.dart
Normal file
76
navigation_and_routing/lib/src/screens/sign_in.dart
Normal 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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
36
navigation_and_routing/lib/src/widgets/author_list.dart
Normal file
36
navigation_and_routing/lib/src/widgets/author_list.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
36
navigation_and_routing/lib/src/widgets/book_list.dart
Normal file
36
navigation_and_routing/lib/src/widgets/book_list.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
25
navigation_and_routing/lib/src/widgets/library_scope.dart
Normal file
25
navigation_and_routing/lib/src/widgets/library_scope.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user