diff --git a/navigation_and_routing/lib/src/app.dart b/navigation_and_routing/lib/src/app.dart index 04ec76454..f930d152d 100644 --- a/navigation_and_routing/lib/src/app.dart +++ b/navigation_and_routing/lib/src/app.dart @@ -2,11 +2,23 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:bookstore/src/auth.dart'; +import 'package:bookstore/src/data.dart'; +import 'package:bookstore/src/screens/author_details.dart'; +import 'package:bookstore/src/screens/book_details.dart'; +import 'package:bookstore/src/screens/books.dart'; +import 'package:bookstore/src/screens/sign_in.dart'; +import 'package:bookstore/src/widgets/fade_transition_page.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; -import 'auth.dart'; -import 'routing.dart'; -import 'screens/navigator.dart'; +import 'screens/authors.dart'; +import 'screens/scaffold.dart'; +import 'screens/settings.dart'; +import 'widgets/book_list.dart'; + +final appShellNavigatorKey = GlobalKey(debugLabel: 'app shell'); +final booksNavigatorKey = GlobalKey(debugLabel: 'books shell'); class Bookstore extends StatefulWidget { const Bookstore({super.key}); @@ -16,97 +28,238 @@ class Bookstore extends StatefulWidget { } class _BookstoreState extends State { - final _auth = BookstoreAuth(); - final _navigatorKey = GlobalKey(); - late final RouteState _routeState; - late final SimpleRouterDelegate _routerDelegate; - late final TemplateRouteParser _routeParser; + final BookstoreAuth auth = BookstoreAuth(); @override - void initState() { - /// Configure the parser with all of the app's allowed path templates. - _routeParser = TemplateRouteParser( - allowedPaths: [ - '/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, - ), - ); - - // Listen for when the user logs out and display the signin screen. - _auth.addListener(_handleAuthStateChanged); - - super.initState(); - } - - @override - Widget build(BuildContext context) => RouteStateScope( - notifier: _routeState, - child: BookstoreAuthScope( - notifier: _auth, - child: MaterialApp.router( - routerDelegate: _routerDelegate, - routeInformationParser: _routeParser, - // Revert back to pre-Flutter-2.5 transition behavior: - // https://github.com/flutter/flutter/issues/82053 - theme: ThemeData( - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), - TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(), - TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), - TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(), + Widget build(BuildContext context) { + return MaterialApp.router( + builder: (context, child) { + if (child == null) { + throw ('No child in .router constructor builder'); + } + return BookstoreAuthScope( + notifier: auth, + child: child, + ); + }, + routerConfig: GoRouter( + refreshListenable: auth, + debugLogDiagnostics: true, + initialLocation: '/books/popular', + redirect: (context, state) { + final signedIn = BookstoreAuth.of(context).signedIn; + if (state.location != '/sign-in' && !signedIn) { + return '/sign-in'; + } + return null; + }, + routes: [ + ShellRoute( + navigatorKey: appShellNavigatorKey, + builder: (context, state, child) { + return BookstoreScaffold( + child: child, + ); + }, + routes: [ + ShellRoute( + pageBuilder: (context, state, child) { + return FadeTransitionPage( + key: state.pageKey, + // Use a builder to get the correct BuildContext + // TODO (johnpryan): remove when https://github.com/flutter/flutter/issues/108177 lands + child: Builder(builder: (context) { + return BooksScreen( + child: child, + onTap: (idx) { + switch (idx) { + case 1: + GoRouter.of(context).go('/books/new'); + break; + case 2: + GoRouter.of(context).go('/books/all'); + break; + case 0: + default: + GoRouter.of(context).go('/books/popular'); + } + }, + ); + }), + ); + }, + routes: [ + GoRoute( + path: '/books/popular', + pageBuilder: (context, state) { + return FadeTransitionPage( + // Use a builder to get the correct BuildContext + // TODO (johnpryan): remove when https://github.com/flutter/flutter/issues/108177 lands + key: state.pageKey, + child: Builder( + builder: (context) { + return BookList( + books: libraryInstance.popularBooks, + onTap: (book) { + GoRouter.of(context) + .go('/books/popular/book/${book.id}'); + }, + ); + }, + ), + ); + }, + routes: [ + GoRoute( + path: 'book/:bookId', + parentNavigatorKey: appShellNavigatorKey, + builder: (context, state) { + return BookDetailsScreen( + book: libraryInstance + .getBook(state.params['bookId'] ?? ''), + ); + }, + ), + ], + ), + GoRoute( + path: '/books/new', + pageBuilder: (context, state) { + return FadeTransitionPage( + key: state.pageKey, + // Use a builder to get the correct BuildContext + // TODO (johnpryan): remove when https://github.com/flutter/flutter/issues/108177 lands + child: Builder( + builder: (context) { + return BookList( + books: libraryInstance.newBooks, + onTap: (book) { + GoRouter.of(context) + .go('/books/new/book/${book.id}'); + }, + ); + }, + ), + ); + }, + routes: [ + GoRoute( + path: 'book/:bookId', + parentNavigatorKey: appShellNavigatorKey, + builder: (context, state) { + return BookDetailsScreen( + book: libraryInstance + .getBook(state.params['bookId'] ?? ''), + ); + }, + ), + ], + ), + GoRoute( + path: '/books/all', + pageBuilder: (context, state) { + return FadeTransitionPage( + key: state.pageKey, + // Use a builder to get the correct BuildContext + // TODO (johnpryan): remove when https://github.com/flutter/flutter/issues/108177 lands + child: Builder( + builder: (context) { + return BookList( + books: libraryInstance.allBooks, + onTap: (book) { + GoRouter.of(context) + .go('/books/all/book/${book.id}'); + }, + ); + }, + ), + ); + }, + routes: [ + GoRoute( + path: 'book/:bookId', + parentNavigatorKey: appShellNavigatorKey, + builder: (context, state) { + return BookDetailsScreen( + book: libraryInstance + .getBook(state.params['bookId'] ?? ''), + ); + }, + ), + ], + ), + ], + ), + GoRoute( + path: '/authors', + pageBuilder: (context, state) { + return FadeTransitionPage( + key: state.pageKey, + child: Builder(builder: (context) { + return AuthorsScreen( + onTap: (author) { + GoRouter.of(context) + .go('/authors/author/${author.id}'); + }, + ); + }), + ); }, + routes: [ + GoRoute( + path: 'author/:authorId', + builder: (context, state) { + final author = libraryInstance.allAuthors.firstWhere( + (author) => + author.id == + int.parse(state.params['authorId']!)); + // Use a builder to get the correct BuildContext + // TODO (johnpryan): remove when https://github.com/flutter/flutter/issues/108177 lands + return Builder(builder: (context) { + return AuthorDetailsScreen( + author: author, + onBookTapped: (book) { + GoRouter.of(context) + .go('/books/all/book/${book.id}'); + }, + ); + }); + }, + ) + ], ), - ), + GoRoute( + path: '/settings', + pageBuilder: (context, state) { + return FadeTransitionPage( + key: state.pageKey, + child: const SettingsScreen(), + ); + }, + ), + ], ), - ), - ); - - Future _guard(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; - } - - void _handleAuthStateChanged() { - if (!_auth.signedIn) { - _routeState.go('/signin'); - } - } - - @override - void dispose() { - _auth.removeListener(_handleAuthStateChanged); - _routeState.dispose(); - _routerDelegate.dispose(); - super.dispose(); + GoRoute( + path: '/sign-in', + builder: (context, state) { + // Use a builder to get the correct BuildContext + // TODO (johnpryan): remove when https://github.com/flutter/flutter/issues/108177 lands + return Builder( + builder: (context) { + return SignInScreen( + onSignIn: (value) async { + final router = GoRouter.of(context); + await BookstoreAuth.of(context) + .signIn(value.username, value.password); + router.go('/books/popular'); + }, + ); + }, + ); + }, + ), + ], + ), + ); } } diff --git a/navigation_and_routing/lib/src/auth.dart b/navigation_and_routing/lib/src/auth.dart index e037369de..8c344f1ce 100644 --- a/navigation_and_routing/lib/src/auth.dart +++ b/navigation_and_routing/lib/src/auth.dart @@ -32,6 +32,10 @@ class BookstoreAuth extends ChangeNotifier { @override int get hashCode => _signedIn.hashCode; + + static BookstoreAuth of(BuildContext context) => context + .dependOnInheritedWidgetOfExactType()! + .notifier!; } class BookstoreAuthScope extends InheritedNotifier { @@ -41,7 +45,4 @@ class BookstoreAuthScope extends InheritedNotifier { super.key, }); - static BookstoreAuth of(BuildContext context) => context - .dependOnInheritedWidgetOfExactType()! - .notifier!; } diff --git a/navigation_and_routing/lib/src/data/library.dart b/navigation_and_routing/lib/src/data/library.dart index 14fd4961f..d52f1bc26 100644 --- a/navigation_and_routing/lib/src/data/library.dart +++ b/navigation_and_routing/lib/src/data/library.dart @@ -51,6 +51,10 @@ class Library { allBooks.add(book); } + Book getBook(String id) { + return allBooks[int.parse(id)]; + } + List get popularBooks => [ ...allBooks.where((book) => book.isPopular), ]; diff --git a/navigation_and_routing/lib/src/routing.dart b/navigation_and_routing/lib/src/routing.dart deleted file mode 100644 index cf0be2c0a..000000000 --- a/navigation_and_routing/lib/src/routing.dart +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2021, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -export 'routing/delegate.dart'; -export 'routing/parsed_route.dart'; -export 'routing/parser.dart'; -export 'routing/route_state.dart'; diff --git a/navigation_and_routing/lib/src/routing/delegate.dart b/navigation_and_routing/lib/src/routing/delegate.dart deleted file mode 100644 index 80d83d5d2..000000000 --- a/navigation_and_routing/lib/src/routing/delegate.dart +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2021, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; - -import 'parsed_route.dart'; -import 'route_state.dart'; - -class SimpleRouterDelegate extends RouterDelegate - with ChangeNotifier, PopNavigatorRouterDelegateMixin { - final RouteState routeState; - final WidgetBuilder builder; - - @override - final GlobalKey navigatorKey; - - SimpleRouterDelegate({ - required this.routeState, - required this.builder, - required this.navigatorKey, - }) { - routeState.addListener(notifyListeners); - } - - @override - Widget build(BuildContext context) => builder(context); - - @override - Future setNewRoutePath(ParsedRoute configuration) async { - routeState.route = configuration; - return SynchronousFuture(null); - } - - @override - ParsedRoute get currentConfiguration => routeState.route; - - @override - void dispose() { - routeState.removeListener(notifyListeners); - routeState.dispose(); - super.dispose(); - } -} diff --git a/navigation_and_routing/lib/src/routing/parsed_route.dart b/navigation_and_routing/lib/src/routing/parsed_route.dart deleted file mode 100644 index 9a40ac759..000000000 --- a/navigation_and_routing/lib/src/routing/parsed_route.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2021, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:collection/collection.dart'; -import 'package:quiver/core.dart'; - -import 'parser.dart'; - -/// A route path that has been parsed by [TemplateRouteParser]. -class ParsedRoute { - /// The current path location without query parameters. (/book/123) - final String path; - - /// The path template (/book/:id) - final String pathTemplate; - - /// The path parameters ({id: 123}) - final Map parameters; - - /// The query parameters ({search: abc}) - final Map queryParameters; - - static const _mapEquality = MapEquality(); - - ParsedRoute( - this.path, this.pathTemplate, this.parameters, this.queryParameters); - - @override - bool operator ==(Object other) => - other is ParsedRoute && - other.pathTemplate == pathTemplate && - other.path == path && - _mapEquality.equals(parameters, other.parameters) && - _mapEquality.equals(queryParameters, other.queryParameters); - - @override - int get hashCode => hash4( - path, - pathTemplate, - _mapEquality.hash(parameters), - _mapEquality.hash(queryParameters), - ); - - @override - String toString() => ''; -} diff --git a/navigation_and_routing/lib/src/routing/parser.dart b/navigation_and_routing/lib/src/routing/parser.dart deleted file mode 100644 index 0c57d5dc6..000000000 --- a/navigation_and_routing/lib/src/routing/parser.dart +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2021, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:flutter/widgets.dart'; -import 'package:path_to_regexp/path_to_regexp.dart'; - -import 'parsed_route.dart'; - -/// Used by [TemplateRouteParser] to guard access to routes. -typedef RouteGuard = Future Function(T from); - -/// Parses the URI path into a [ParsedRoute]. -class TemplateRouteParser extends RouteInformationParser { - final List _pathTemplates; - final RouteGuard? guard; - final ParsedRoute initialRoute; - - TemplateRouteParser({ - /// The list of allowed path templates (['/', '/users/:id']) - required List allowedPaths, - - /// The initial route - String initialRoute = '/', - - /// [RouteGuard] used to redirect. - this.guard, - }) : initialRoute = ParsedRoute(initialRoute, initialRoute, {}, {}), - _pathTemplates = [ - ...allowedPaths, - ], - assert(allowedPaths.contains(initialRoute)); - - @override - Future parseRouteInformation( - RouteInformation routeInformation, - ) async { - final path = routeInformation.location!; - final queryParams = Uri.parse(path).queryParameters; - var parsedRoute = initialRoute; - - for (var pathTemplate in _pathTemplates) { - final parameters = []; - var pathRegExp = pathToRegExp(pathTemplate, parameters: parameters); - if (pathRegExp.hasMatch(path)) { - final match = pathRegExp.matchAsPrefix(path); - if (match == null) continue; - final params = extract(parameters, match); - parsedRoute = ParsedRoute(path, pathTemplate, params, queryParams); - } - } - - // Redirect if a guard is present - var guard = this.guard; - if (guard != null) { - return guard(parsedRoute); - } - - return parsedRoute; - } - - @override - RouteInformation restoreRouteInformation(ParsedRoute configuration) => - RouteInformation(location: configuration.path); -} diff --git a/navigation_and_routing/lib/src/routing/route_state.dart b/navigation_and_routing/lib/src/routing/route_state.dart deleted file mode 100644 index 56a10d091..000000000 --- a/navigation_and_routing/lib/src/routing/route_state.dart +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2021, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:flutter/widgets.dart'; - -import 'parsed_route.dart'; -import 'parser.dart'; - -/// The current route state. To change the current route, call obtain the state -/// using `RouteStateScope.of(context)` and call `go()`: -/// -/// ``` -/// RouteStateScope.of(context).go('/book/2'); -/// ``` -class RouteState extends ChangeNotifier { - final TemplateRouteParser _parser; - ParsedRoute _route; - - RouteState(this._parser) : _route = _parser.initialRoute; - - ParsedRoute get route => _route; - - set route(ParsedRoute route) { - // Don't notify listeners if the path hasn't changed. - if (_route == route) return; - - _route = route; - notifyListeners(); - } - - Future go(String route) async { - this.route = - await _parser.parseRouteInformation(RouteInformation(location: route)); - } -} - -/// Provides the current [RouteState] to descendant widgets in the tree. -class RouteStateScope extends InheritedNotifier { - const RouteStateScope({ - required super.notifier, - required super.child, - super.key, - }); - - static RouteState of(BuildContext context) => - context.dependOnInheritedWidgetOfExactType()!.notifier!; -} diff --git a/navigation_and_routing/lib/src/screens/author_details.dart b/navigation_and_routing/lib/src/screens/author_details.dart index d45f861b5..ba3043fea 100644 --- a/navigation_and_routing/lib/src/screens/author_details.dart +++ b/navigation_and_routing/lib/src/screens/author_details.dart @@ -5,15 +5,16 @@ import 'package:flutter/material.dart'; import '../data.dart'; -import '../routing.dart'; import '../widgets/book_list.dart'; class AuthorDetailsScreen extends StatelessWidget { final Author author; + final ValueChanged onBookTapped; const AuthorDetailsScreen({ super.key, required this.author, + required this.onBookTapped, }); @override @@ -28,7 +29,7 @@ class AuthorDetailsScreen extends StatelessWidget { child: BookList( books: author.books, onTap: (book) { - RouteStateScope.of(context).go('/book/${book.id}'); + onBookTapped(book); }, ), ), diff --git a/navigation_and_routing/lib/src/screens/authors.dart b/navigation_and_routing/lib/src/screens/authors.dart index 8965f91d9..52ce67019 100644 --- a/navigation_and_routing/lib/src/screens/authors.dart +++ b/navigation_and_routing/lib/src/screens/authors.dart @@ -4,14 +4,18 @@ import 'package:flutter/material.dart'; +import '../data/author.dart'; import '../data/library.dart'; -import '../routing.dart'; import '../widgets/author_list.dart'; class AuthorsScreen extends StatelessWidget { final String title = 'Authors'; + ValueChanged onTap; - const AuthorsScreen({super.key}); + AuthorsScreen({ + required this.onTap, + super.key, + }); @override Widget build(BuildContext context) => Scaffold( @@ -20,9 +24,7 @@ class AuthorsScreen extends StatelessWidget { ), body: AuthorList( authors: libraryInstance.allAuthors, - onTap: (author) { - RouteStateScope.of(context).go('/author/${author.id}'); - }, + onTap: onTap, ), ); } diff --git a/navigation_and_routing/lib/src/screens/book_details.dart b/navigation_and_routing/lib/src/screens/book_details.dart index 72a934686..4f43d5872 100644 --- a/navigation_and_routing/lib/src/screens/book_details.dart +++ b/navigation_and_routing/lib/src/screens/book_details.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:url_launcher/link.dart'; import '../data.dart'; @@ -45,14 +46,18 @@ class BookDetailsScreen extends StatelessWidget { onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => - AuthorDetailsScreen(author: book!.author), + builder: (context) => AuthorDetailsScreen( + author: book!.author, + onBookTapped: (Book book) { + GoRouter.of(context).go('/books/all/book/${book.id}'); + }, + ), ), ); }, ), Link( - uri: Uri.parse('/author/${book!.author.id}'), + uri: Uri.parse('/authors/author/${book!.author.id}'), builder: (context, followLink) => TextButton( onPressed: followLink, child: const Text('View author (Link)'), diff --git a/navigation_and_routing/lib/src/screens/books.dart b/navigation_and_routing/lib/src/screens/books.dart index 4fc6509f0..be89e1dca 100644 --- a/navigation_and_routing/lib/src/screens/books.dart +++ b/navigation_and_routing/lib/src/screens/books.dart @@ -3,13 +3,15 @@ // 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 'package:go_router/go_router.dart'; class BooksScreen extends StatefulWidget { + final Widget child; + final ValueChanged onTap; + const BooksScreen({ + required this.child, + required this.onTap, super.key, }); @@ -28,20 +30,6 @@ class _BooksScreenState extends State ..addListener(_handleTabIndexChanged); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - final newPath = _routeState.route.pathTemplate; - if (newPath.startsWith('/books/popular')) { - _tabController.index = 0; - } else if (newPath.startsWith('/books/new')) { - _tabController.index = 1; - } else if (newPath == '/books/all') { - _tabController.index = 2; - } - } - @override void dispose() { _tabController.removeListener(_handleTabIndexChanged); @@ -49,64 +37,43 @@ class _BooksScreenState extends State } @override - Widget build(BuildContext context) => 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( + Widget build(BuildContext context) { + _tabController.index = _getSelectedIndex(GoRouter.of(context).location); + return Scaffold( + appBar: AppBar( + title: const Text('Books'), + bottom: TabBar( controller: _tabController, - children: [ - BookList( - books: libraryInstance.popularBooks, - onTap: _handleBookTapped, + tabs: const [ + Tab( + text: 'Popular', + icon: Icon(Icons.people), ), - BookList( - books: libraryInstance.newBooks, - onTap: _handleBookTapped, + Tab( + text: 'New', + icon: Icon(Icons.new_releases), ), - BookList( - books: libraryInstance.allBooks, - onTap: _handleBookTapped, + Tab( + text: 'All', + icon: Icon(Icons.list), ), ], ), - ); - - RouteState get _routeState => RouteStateScope.of(context); - - void _handleBookTapped(Book book) { - _routeState.go('/book/${book.id}'); + ), + body: widget.child, + ); } 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; + if (_tabController.index != _getSelectedIndex(GoRouter.of(context).location)) { + widget.onTap(_tabController.index); } } + + int _getSelectedIndex(String pathTemplate) { + if (pathTemplate.startsWith('/books/popular')) return 0; + if (pathTemplate.startsWith('/books/new')) return 1; + if (pathTemplate.startsWith('/books/all')) return 2; + return 0; + } } diff --git a/navigation_and_routing/lib/src/screens/navigator.dart b/navigation_and_routing/lib/src/screens/navigator.dart deleted file mode 100644 index 0f62f503e..000000000 --- a/navigation_and_routing/lib/src/screens/navigator.dart +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2021, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - -import '../auth.dart'; -import '../data.dart'; -import '../routing.dart'; -import '../screens/sign_in.dart'; -import '../widgets/fade_transition_page.dart'; -import 'author_details.dart'; -import 'book_details.dart'; -import 'scaffold.dart'; - -/// Builds the top-level navigator for the app. The pages to display are based -/// on the `routeState` that was parsed by the TemplateRouteParser. -class BookstoreNavigator extends StatefulWidget { - final GlobalKey navigatorKey; - - const BookstoreNavigator({ - required this.navigatorKey, - super.key, - }); - - @override - State createState() => _BookstoreNavigatorState(); -} - -class _BookstoreNavigatorState extends State { - final _signInKey = const ValueKey('Sign in'); - final _scaffoldKey = const ValueKey('App scaffold'); - final _bookDetailsKey = const ValueKey('Book details screen'); - final _authorDetailsKey = const ValueKey('Author details screen'); - - @override - Widget build(BuildContext context) { - final routeState = RouteStateScope.of(context); - final authState = BookstoreAuthScope.of(context); - final pathTemplate = routeState.route.pathTemplate; - - Book? selectedBook; - if (pathTemplate == '/book/:bookId') { - selectedBook = libraryInstance.allBooks.firstWhereOrNull( - (b) => b.id.toString() == routeState.route.parameters['bookId']); - } - - Author? selectedAuthor; - if (pathTemplate == '/author/:authorId') { - selectedAuthor = libraryInstance.allAuthors.firstWhereOrNull( - (b) => b.id.toString() == routeState.route.parameters['authorId']); - } - - return Navigator( - key: widget.navigatorKey, - onPopPage: (route, dynamic result) { - // When a page that is stacked on top of the scaffold is popped, display - // the /books or /authors tab in BookstoreScaffold. - if (route.settings is Page && - (route.settings as Page).key == _bookDetailsKey) { - routeState.go('/books/popular'); - } - - if (route.settings is Page && - (route.settings as Page).key == _authorDetailsKey) { - routeState.go('/authors'); - } - - return route.didPop(result); - }, - pages: [ - if (routeState.route.pathTemplate == '/signin') - // Display the sign in screen. - FadeTransitionPage( - key: _signInKey, - child: SignInScreen( - onSignIn: (credentials) async { - var signedIn = await authState.signIn( - credentials.username, credentials.password); - if (signedIn) { - await routeState.go('/books/popular'); - } - }, - ), - ) - else ...[ - // Display the app - FadeTransitionPage( - key: _scaffoldKey, - child: const BookstoreScaffold(), - ), - // Add an additional page to the stack if the user is viewing a book - // or an author - if (selectedBook != null) - MaterialPage( - key: _bookDetailsKey, - child: BookDetailsScreen( - book: selectedBook, - ), - ) - else if (selectedAuthor != null) - MaterialPage( - key: _authorDetailsKey, - child: AuthorDetailsScreen( - author: selectedAuthor, - ), - ), - ], - ], - ); - } -} diff --git a/navigation_and_routing/lib/src/screens/scaffold.dart b/navigation_and_routing/lib/src/screens/scaffold.dart index 494919192..dffba6da1 100644 --- a/navigation_and_routing/lib/src/screens/scaffold.dart +++ b/navigation_and_routing/lib/src/screens/scaffold.dart @@ -4,28 +4,29 @@ import 'package:adaptive_navigation/adaptive_navigation.dart'; import 'package:flutter/material.dart'; - -import '../routing.dart'; -import 'scaffold_body.dart'; +import 'package:go_router/go_router.dart'; class BookstoreScaffold extends StatelessWidget { + final Widget child; + const BookstoreScaffold({ + required this.child, super.key, }); @override Widget build(BuildContext context) { - final routeState = RouteStateScope.of(context); - final selectedIndex = _getSelectedIndex(routeState.route.pathTemplate); + final goRouter = GoRouter.of(context); + final selectedIndex = _getSelectedIndex(goRouter.location); return Scaffold( body: AdaptiveNavigationScaffold( selectedIndex: selectedIndex, - body: const BookstoreScaffoldBody(), + body: child, onDestinationSelected: (idx) { - if (idx == 0) routeState.go('/books/popular'); - if (idx == 1) routeState.go('/authors'); - if (idx == 2) routeState.go('/settings'); + if (idx == 0) goRouter.go('/books/popular'); + if (idx == 1) goRouter.go('/authors'); + if (idx == 2) goRouter.go('/settings'); }, destinations: const [ AdaptiveScaffoldDestination( diff --git a/navigation_and_routing/lib/src/screens/scaffold_body.dart b/navigation_and_routing/lib/src/screens/scaffold_body.dart deleted file mode 100644 index 868e32ef7..000000000 --- a/navigation_and_routing/lib/src/screens/scaffold_body.dart +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2021, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:flutter/material.dart'; - -import '../routing.dart'; -import '../screens/settings.dart'; -import '../widgets/fade_transition_page.dart'; -import 'authors.dart'; -import 'books.dart'; -import 'scaffold.dart'; - -/// Displays the contents of the body of [BookstoreScaffold] -class BookstoreScaffoldBody extends StatelessWidget { - static GlobalKey navigatorKey = GlobalKey(); - - const BookstoreScaffoldBody({ - super.key, - }); - - @override - Widget build(BuildContext context) { - var currentRoute = RouteStateScope.of(context).route; - - // A nested Router isn't necessary because the back button behavior doesn't - // need to be customized. - return Navigator( - key: navigatorKey, - onPopPage: (route, dynamic result) => route.didPop(result), - pages: [ - if (currentRoute.pathTemplate.startsWith('/authors')) - const FadeTransitionPage( - key: ValueKey('authors'), - child: AuthorsScreen(), - ) - else if (currentRoute.pathTemplate.startsWith('/settings')) - const FadeTransitionPage( - key: ValueKey('settings'), - child: SettingsScreen(), - ) - else if (currentRoute.pathTemplate.startsWith('/books') || - currentRoute.pathTemplate == '/') - const FadeTransitionPage( - key: ValueKey('books'), - child: BooksScreen(), - ) - - // Avoid building a Navigator with an empty `pages` list when the - // RouteState is set to an unexpected path, such as /signin. - // - // Since RouteStateScope is an InheritedNotifier, any change to the - // route will result in a call to this build method, even though this - // widget isn't built when those routes are active. - else - FadeTransitionPage( - key: const ValueKey('empty'), - child: Container(), - ), - ], - ); - } -} diff --git a/navigation_and_routing/lib/src/screens/settings.dart b/navigation_and_routing/lib/src/screens/settings.dart index d25ec1ba2..c4fabb691 100644 --- a/navigation_and_routing/lib/src/screens/settings.dart +++ b/navigation_and_routing/lib/src/screens/settings.dart @@ -3,10 +3,10 @@ // BSD-style license that can be found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:url_launcher/link.dart'; import '../auth.dart'; -import '../routing.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -52,24 +52,27 @@ class SettingsContent extends StatelessWidget { ), ElevatedButton( onPressed: () { - BookstoreAuthScope.of(context).signOut(); + BookstoreAuth.of(context).signOut(); }, child: const Text('Sign out'), ), + const Text('Example using the Link widget:'), Link( - uri: Uri.parse('/book/0'), + uri: Uri.parse('/books/all/book/0'), builder: (context, followLink) => TextButton( onPressed: followLink, - child: const Text('Go directly to /book/0 (Link)'), + child: const Text('/books/all/book/0'), ), ), + const Text('Example using GoRouter.of(context).go():'), TextButton( - child: const Text('Go directly to /book/0 (RouteState)'), + child: const Text('/books/all/book/0'), onPressed: () { - RouteStateScope.of(context).go('/book/0'); + GoRouter.of(context).go('/books/all/book/0'); }, ), ].map((w) => Padding(padding: const EdgeInsets.all(8), child: w)), + const Text('Displays a dialog on the root Navigator:'), TextButton( onPressed: () => showDialog( context: context, diff --git a/navigation_and_routing/pubspec.yaml b/navigation_and_routing/pubspec.yaml index 61c1b1509..ade4fdf98 100644 --- a/navigation_and_routing/pubspec.yaml +++ b/navigation_and_routing/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: cupertino_icons: ^1.0.2 flutter: sdk: flutter + go_router: ^5.0.0 path_to_regexp: ^0.4.0 quiver: ^3.1.0 url_launcher: ^6.1.1 @@ -28,4 +29,4 @@ dev_dependencies: test: ^1.16.0 flutter: - uses-material-design: true + uses-material-design: true \ No newline at end of file