diff --git a/navigation_and_routing/README.md b/navigation_and_routing/README.md index 8502eee55..676ceb59c 100644 --- a/navigation_and_routing/README.md +++ b/navigation_and_routing/README.md @@ -1,148 +1,12 @@ # Navigation and Routing -A sample that shows how to use the [Router][] API to handle common navigation -scenarios. +A sample that shows how to use [go_router][https://pub.dev/packages/go_router] +API to handle common navigation scenarios. ## Goals - Demonstrate common navigation scenarios: - Parsing path parameters ('/user/:id') - - Sign in (validation / guards) - - Nested navigation -- Provide a reusable implementation of RouterDelegate and RouteInformationParser + - Sign in (redirection) + - Nested navigation using ShellRoute - Demonstrate how [deep linking][] is configured on iOS and Android - Demonstrate how to use the Link widget from `package:url_Launcher` with the Router API. - -## How it works -The top-level widget, `Bookstore`, sets up the state for this app. It places -three `InheritedNotifier` widgets in the tree: `RouteStateScope`, -`BookstoreAuthScope`, and `LibraryScope`, which provide the state for the -application: - - - **`RouteState`**: stores the current route path (`/book/1`) as a `ParsedRoute` - object (see below). - - **`BookstoreAuthScope`**: stores a mock authentication API, `BookstoreAuth`. - - **`LibraryScope`**: stores the data for the app, `Library`. - -The `Bookstore` widget also uses the [MaterialApp.router()][router-ctor] -constructor to opt-in to the [Router][] API. This constructor requires a -[RouterDelegate][] and [RouteInformationParser][]. This app uses the -`routing.dart` library, described below. - -## routing.dart -This library contains a general-purpose routing solution for medium-sized apps. -It implements these classes: - -- **`SimpleRouterDelegate`**: Implements `RouterDelegate`. Updates `RouteState` when - a new route has been pushed to the application by the operating system. Also - notifies the `Router` widget whenever the `RouteState` changes. -- **`TemplateRouteParser`**: Implements RouteInformationParser. Parses the - incoming route path into a `ParsedRoute` object. A `RouteGuard` can be - provided to guard access to certain routes. -- **`ParsedRoute`**: Contains the current route location ("/user/2"), path - parameters ({id: 2}), query parameters ("?search=abc"), and path template - ("/user/:id") -- **`RouteState`**: Stores the current `ParsedRoute`. -- **`RouteGuard`**: Guards access to routes. Can be overridden to redirect the - incoming route if a condition isn't met. - -## App Structure - -The `SimpleRouterDelegate` constructor requires a `WidgetBuilder` parameter and -a `navigatorKey`. This app uses a `BookstoreNavigator` widget, which configures -a `Navigator` with a list of pages, based on the current `RouteState`. - -```dart -SimpleRouterDelegate( - routeState: routeState, - navigatorKey: navigatorKey, - builder: (context) => BookstoreNavigator( - navigatorKey: navigatorKey, - ), -); -``` - -This `Navigator` is configured to display either the sign-in screen or the -`BookstoreScaffold`. An additional screen is stacked on top of the -`BookstoreScaffold` if a book or author is currently selected: - -```dart -return Navigator( - key: widget.navigatorKey, - onPopPage: (route, dynamic result) { - // ... - }, - pages: [ - if (routeState.route.pathTemplate == '/signin') - FadeTransitionPage( - key: signInKey, - child: SignInScreen(), - ), - else ...[ - FadeTransitionPage( - key: scaffoldKey, - child: BookstoreScaffold(), - ), - if (selectedBook != null) - MaterialPage( - key: bookDetailsKey, - child: BookDetailsScreen( - book: selectedBook, - ), - ) - else if (selectedAuthor != null) - MaterialPage( - key: authorDetailsKey, - child: AuthorDetailsScreen( - author: selectedAuthor, - ), - ), - ], - ], -); -``` - -The `BookstoreScaffold` widget uses `package:adaptive_navigation` to build a -navigation rail or bottom navigation bar based on the size of the screen. The -body of this screen is `BookstoreScaffoldBody`, which configures a nested -Navigator to display either the `AuthorsScreen`, `SettingsScreen`, or -`BooksScreen` widget. - -## Linking vs updating RouteState - -There are two ways to change the current route, either by updating `RouteState`, -which the RouterDelegate listens to, or use the Link widget from -`package:url_launcher`. The `SettingsScreen` widget demonstrates both options: - -``` -Link( - uri: Uri.parse('/book/0'), - builder: (context, followLink) { - return TextButton( - child: const Text('Go directly to /book/0 (Link)'), - onPressed: followLink, - ); - }, -), -TextButton( - child: const Text('Go directly to /book/0 (RouteState)'), - onPressed: () { - RouteStateScope.of(context)!.go('/book/0'); - }, -), -``` - -## Questions/issues - -If you have a general question about the Router API, the best places to go are: - -- [The FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev) -- [StackOverflow](https://stackoverflow.com/questions/tagged/flutter) - -If you run into an issue with the sample itself, please file an issue -in the [main Flutter repo](https://github.com/flutter/flutter/issues). - -[Router]: https://api.flutter.dev/flutter/widgets/Router-class.html -[RouterDelegate]: https://api.flutter.dev/flutter/widgets/RouterDelegate-class.html -[RouteInformationParser]: https://api.flutter.dev/flutter/widgets/RouteInformationParser-class.html -[router-ctor]: https://api.flutter.dev/flutter/material/MaterialApp/MaterialApp.router.html -[deep linking]: https://flutter.dev/docs/development/ui/navigation/deep-linking diff --git a/navigation_and_routing/lib/src/app.dart b/navigation_and_routing/lib/src/app.dart index 95dcb3792..1f757b2ca 100644 --- a/navigation_and_routing/lib/src/app.dart +++ b/navigation_and_routing/lib/src/app.dart @@ -3,10 +3,22 @@ // BSD-style license that can be found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'auth.dart'; -import 'routing.dart'; -import 'screens/navigator.dart'; +import 'data.dart'; +import 'screens/author_details.dart'; +import 'screens/authors.dart'; +import 'screens/book_details.dart'; +import 'screens/books.dart'; +import 'screens/scaffold.dart'; +import 'screens/settings.dart'; +import 'screens/sign_in.dart'; +import 'widgets/book_list.dart'; +import 'widgets/fade_transition_page.dart'; + +final appShellNavigatorKey = GlobalKey(debugLabel: 'app shell'); +final booksNavigatorKey = GlobalKey(debugLabel: 'books shell'); class Bookstore extends StatefulWidget { const Bookstore({super.key}); @@ -16,98 +28,245 @@ 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; - - @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(); - } + final BookstoreAuth auth = BookstoreAuth(); @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( - useMaterial3: true, - 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.uri.toString() != '/sign-in' && !signedIn) { + return '/sign-in'; + } + return null; + }, + routes: [ + ShellRoute( + navigatorKey: appShellNavigatorKey, + builder: (context, state, child) { + return BookstoreScaffold( + selectedIndex: switch (state.uri.path) { + var p when p.startsWith('/books') => 0, + var p when p.startsWith('/authors') => 1, + var p when p.startsWith('/settings') => 2, + _ => 0, + }, + 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( + onTap: (idx) { + GoRouter.of(context).go(switch (idx) { + 0 => '/books/popular', + 1 => '/books/new', + 2 => '/books/all', + _ => '/books/popular', + }); + }, + selectedIndex: switch (state.uri.path) { + var p when p.startsWith('/books/popular') => 0, + var p when p.startsWith('/books/new') => 1, + var p when p.startsWith('/books/all') => 2, + _ => 0, + }, + child: child, + ); + }), + ); + }, + 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.pathParameters['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.pathParameters['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.pathParameters['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.pathParameters['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..9804207f4 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 { @@ -40,8 +44,4 @@ class BookstoreAuthScope extends InheritedNotifier { required super.child, 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 00c0757ff..000000000 --- a/navigation_and_routing/lib/src/routing/parser.dart +++ /dev/null @@ -1,66 +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 uri = routeInformation.uri; - final path = uri.toString(); - final queryParams = uri.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(uri: Uri.parse(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 aa7e1adc8..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(uri: Uri.parse(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..100b6096e 100644 --- a/navigation_and_routing/lib/src/screens/authors.dart +++ b/navigation_and_routing/lib/src/screens/authors.dart @@ -4,14 +4,19 @@ 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'; + final String title; + final ValueChanged onTap; - const AuthorsScreen({super.key}); + const AuthorsScreen({ + required this.onTap, + this.title = 'Authors', + super.key, + }); @override Widget build(BuildContext context) => Scaffold( @@ -20,9 +25,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..4e8b97e94 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) { + 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 2646f1a25..6e323ea84 100644 --- a/navigation_and_routing/lib/src/screens/books.dart +++ b/navigation_and_routing/lib/src/screens/books.dart @@ -4,12 +4,15 @@ import 'package:flutter/material.dart'; -import '../data.dart'; -import '../routing.dart'; -import '../widgets/book_list.dart'; - class BooksScreen extends StatefulWidget { + final Widget child; + final ValueChanged onTap; + final int selectedIndex; + const BooksScreen({ + required this.child, + required this.onTap, + required this.selectedIndex, super.key, }); @@ -28,20 +31,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,63 +38,34 @@ class _BooksScreenState extends State } @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Books'), - elevation: 8, - 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 = widget.selectedIndex; + 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'); - case 2: - _routeState.go('/books/all'); - case 0: - default: - _routeState.go('/books/popular'); - break; - } + widget.onTap(_tabController.index); } } 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..678fbb913 100644 --- a/navigation_and_routing/lib/src/screens/scaffold.dart +++ b/navigation_and_routing/lib/src/screens/scaffold.dart @@ -4,28 +4,30 @@ 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; + final int selectedIndex; + const BookstoreScaffold({ + required this.child, + required this.selectedIndex, super.key, }); @override Widget build(BuildContext context) { - final routeState = RouteStateScope.of(context); - final selectedIndex = _getSelectedIndex(routeState.route.pathTemplate); + final goRouter = GoRouter.of(context); 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( @@ -44,11 +46,4 @@ class BookstoreScaffold extends StatelessWidget { ), ); } - - int _getSelectedIndex(String pathTemplate) { - if (pathTemplate.startsWith('/books')) return 0; - if (pathTemplate == '/authors') return 1; - if (pathTemplate == '/settings') return 2; - return 0; - } } 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 3519f3323..4e0972805 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 { ), FilledButton( 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 cdd5064f5..29b295376 100644 --- a/navigation_and_routing/pubspec.yaml +++ b/navigation_and_routing/pubspec.yaml @@ -4,14 +4,15 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ^3.1.0 + sdk: ">=3.0.0 <4.0.0" dependencies: adaptive_navigation: ^0.0.3 - collection: ^1.15.0 + collection: ^1.17.0 cupertino_icons: ^1.0.2 flutter: sdk: flutter + go_router: ^12.0.0 path_to_regexp: ^0.4.0 quiver: ^3.1.0 url_launcher: ^6.1.1 @@ -26,7 +27,7 @@ dev_dependencies: path: ../analysis_defaults flutter_test: sdk: flutter - test: ^1.16.0 + test: ^1.24.0 flutter: uses-material-design: true