-
-
Save johnpryan/bbca91e23bbb4d39247fa922533be7c9 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(NestedRouterDemo()); | |
} | |
class Book { | |
final String title; | |
final String author; | |
Book(this.title, this.author); | |
} | |
class NestedRouterDemo extends StatefulWidget { | |
@override | |
_NestedRouterDemoState createState() => _NestedRouterDemoState(); | |
} | |
class _NestedRouterDemoState extends State<NestedRouterDemo> { | |
BookRouterDelegate _routerDelegate = BookRouterDelegate(); | |
BookRouteInformationParser _routeInformationParser = | |
BookRouteInformationParser(); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp.router( | |
title: 'Books App', | |
routerDelegate: _routerDelegate, | |
routeInformationParser: _routeInformationParser, | |
); | |
} | |
} | |
class BooksAppState extends ChangeNotifier { | |
int _selectedIndex; | |
Book _selectedBook; | |
final List<Book> books = [ | |
Book('Stranger in a Strange Land', 'Robert A. Heinlein'), | |
Book('Foundation', 'Isaac Asimov'), | |
Book('Fahrenheit 451', 'Ray Bradbury'), | |
]; | |
BooksAppState() : _selectedIndex = 0; | |
int get selectedIndex => _selectedIndex; | |
set selectedIndex(int idx) { | |
_selectedIndex = idx; | |
if (_selectedIndex == 1) { | |
// Remove this line if you want to keep the selected book when navigating | |
// between "settings" and "home" which book was selected when Settings is | |
// tapped. | |
selectedBook = null; | |
} | |
notifyListeners(); | |
} | |
Book get selectedBook => _selectedBook; | |
set selectedBook(Book book) { | |
_selectedBook = book; | |
notifyListeners(); | |
} | |
int getSelectedBookById() { | |
if (!books.contains(_selectedBook)) return 0; | |
return books.indexOf(_selectedBook); | |
} | |
void setSelectedBookById(int id) { | |
if (id < 0 || id > books.length - 1) { | |
return; | |
} | |
_selectedBook = books[id]; | |
notifyListeners(); | |
} | |
} | |
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> { | |
@override | |
Future<BookRoutePath> parseRouteInformation( | |
RouteInformation routeInformation) async { | |
final uri = Uri.parse(routeInformation.location); | |
if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'settings') { | |
return BooksSettingsPath(); | |
} else { | |
if (uri.pathSegments.length >= 2) { | |
if (uri.pathSegments[0] == 'book') { | |
return BooksDetailsPath(int.tryParse(uri.pathSegments[1])); | |
} | |
} | |
return BooksListPath(); | |
} | |
} | |
@override | |
RouteInformation restoreRouteInformation(BookRoutePath configuration) { | |
if (configuration is BooksListPath) { | |
return RouteInformation(location: '/home'); | |
} | |
if (configuration is BooksSettingsPath) { | |
return RouteInformation(location: '/settings'); | |
} | |
if (configuration is BooksDetailsPath) { | |
return RouteInformation(location: '/book/${configuration.id}'); | |
} | |
return null; | |
} | |
} | |
class BookRouterDelegate extends RouterDelegate<BookRoutePath> | |
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> { | |
final GlobalKey<NavigatorState> navigatorKey; | |
BooksAppState appState = BooksAppState(); | |
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() { | |
appState.addListener(notifyListeners); | |
} | |
BookRoutePath get currentConfiguration { | |
if (appState.selectedIndex == 1) { | |
return BooksSettingsPath(); | |
} else { | |
if (appState.selectedBook == null) { | |
return BooksListPath(); | |
} else { | |
return BooksDetailsPath(appState.getSelectedBookById()); | |
} | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Navigator( | |
key: navigatorKey, | |
pages: [ | |
MaterialPage( | |
child: AppShell(appState: appState), | |
), | |
], | |
onPopPage: (route, result) { | |
if (!route.didPop(result)) { | |
return false; | |
} | |
if (appState.selectedBook != null) { | |
appState.selectedBook = null; | |
} | |
notifyListeners(); | |
return true; | |
}, | |
); | |
} | |
@override | |
Future<void> setNewRoutePath(BookRoutePath path) async { | |
if (path is BooksListPath) { | |
appState.selectedIndex = 0; | |
appState.selectedBook = null; | |
} else if (path is BooksSettingsPath) { | |
appState.selectedIndex = 1; | |
} else if (path is BooksDetailsPath) { | |
appState.setSelectedBookById(path.id); | |
} | |
} | |
} | |
// Routes | |
abstract class BookRoutePath {} | |
class BooksListPath extends BookRoutePath {} | |
class BooksSettingsPath extends BookRoutePath {} | |
class BooksDetailsPath extends BookRoutePath { | |
final int id; | |
BooksDetailsPath(this.id); | |
} | |
// Widget that contains the AdaptiveNavigationScaffold | |
class AppShell extends StatefulWidget { | |
final BooksAppState appState; | |
AppShell({ | |
@required this.appState, | |
}); | |
@override | |
_AppShellState createState() => _AppShellState(); | |
} | |
class _AppShellState extends State<AppShell> { | |
InnerRouterDelegate _routerDelegate; | |
ChildBackButtonDispatcher _backButtonDispatcher; | |
void initState() { | |
super.initState(); | |
_routerDelegate = InnerRouterDelegate(widget.appState); | |
} | |
@override | |
void didUpdateWidget(covariant AppShell oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
_routerDelegate.appState = widget.appState; | |
} | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
// Defer back button dispatching to the child router | |
_backButtonDispatcher = Router.of(context) | |
.backButtonDispatcher | |
.createChildBackButtonDispatcher(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
var appState = widget.appState; | |
// Claim priority, If there are parallel sub router, you will need | |
// to pick which one should take priority; | |
_backButtonDispatcher.takePriority(); | |
return Scaffold( | |
appBar: AppBar(), | |
body: Router( | |
routerDelegate: _routerDelegate, | |
backButtonDispatcher: _backButtonDispatcher, | |
), | |
bottomNavigationBar: BottomNavigationBar( | |
items: [ | |
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), | |
BottomNavigationBarItem( | |
icon: Icon(Icons.settings), label: 'Settings'), | |
], | |
currentIndex: appState.selectedIndex, | |
onTap: (newIndex) { | |
appState.selectedIndex = newIndex; | |
}, | |
), | |
); | |
} | |
} | |
class InnerRouterDelegate extends RouterDelegate<BookRoutePath> | |
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> { | |
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); | |
BooksAppState get appState => _appState; | |
BooksAppState _appState; | |
set appState(BooksAppState value) { | |
if (value == _appState) { | |
return; | |
} | |
_appState = value; | |
notifyListeners(); | |
} | |
InnerRouterDelegate(this._appState); | |
@override | |
Widget build(BuildContext context) { | |
return Navigator( | |
key: navigatorKey, | |
pages: [ | |
if (appState.selectedIndex == 0) ...[ | |
FadeAnimationPage( | |
child: BooksListScreen( | |
books: appState.books, | |
onTapped: _handleBookTapped, | |
), | |
key: ValueKey('BooksListPage'), | |
), | |
if (appState.selectedBook != null) | |
MaterialPage( | |
key: ValueKey(appState.selectedBook), | |
child: BookDetailsScreen(book: appState.selectedBook), | |
), | |
] else | |
FadeAnimationPage( | |
child: SettingsScreen(), | |
key: ValueKey('SettingsPage'), | |
), | |
], | |
onPopPage: (route, result) { | |
appState.selectedBook = null; | |
notifyListeners(); | |
return route.didPop(result); | |
}, | |
); | |
} | |
@override | |
Future<void> setNewRoutePath(BookRoutePath path) async { | |
// This is not required for inner router delegate because it does not | |
// parse route | |
assert(false); | |
} | |
void _handleBookTapped(Book book) { | |
appState.selectedBook = book; | |
notifyListeners(); | |
} | |
} | |
class FadeAnimationPage extends Page { | |
final Widget child; | |
FadeAnimationPage({Key key, this.child}) : super(key: key); | |
Route createRoute(BuildContext context) { | |
return PageRouteBuilder( | |
settings: this, | |
pageBuilder: (context, animation, animation2) { | |
var curveTween = CurveTween(curve: Curves.easeIn); | |
return FadeTransition( | |
opacity: animation.drive(curveTween), | |
child: child, | |
); | |
}, | |
); | |
} | |
} | |
// Screens | |
class BooksListScreen extends StatelessWidget { | |
final List<Book> books; | |
final ValueChanged<Book> onTapped; | |
BooksListScreen({ | |
@required this.books, | |
@required this.onTapped, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: ListView( | |
children: [ | |
for (var book in books) | |
ListTile( | |
title: Text(book.title), | |
subtitle: Text(book.author), | |
onTap: () => onTapped(book), | |
) | |
], | |
), | |
); | |
} | |
} | |
class BookDetailsScreen extends StatelessWidget { | |
final Book book; | |
BookDetailsScreen({ | |
@required this.book, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
FlatButton( | |
onPressed: () { | |
Navigator.of(context).pop(); | |
}, | |
child: Text('Back'), | |
), | |
if (book != null) ...[ | |
Text(book.title, style: Theme.of(context).textTheme.headline6), | |
Text(book.author, style: Theme.of(context).textTheme.subtitle1), | |
], | |
], | |
), | |
), | |
); | |
} | |
} | |
class SettingsScreen extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Center( | |
child: Text('Settings screen'), | |
), | |
); | |
} | |
} |
Why the inner Router
cannot be wired to parse route?
This would be much simpler if we could just do
MaterialApp(
home: Scaffold(
body: Router(
routerDelegate: _myRouterDelegate,
routeInformationParser: _myRouteInformationParser,
routeInformationProvider: PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(location: '/my-initial-route'),
),
backButtonDispatcher: RootBackButtonDispatcher(),
),
bottomNavigationBar: ...,
),
)
@jonasjuni I've a tweak for adding the back button on the AppBar based on this sample code. Please check it out.
- In the
_AppShellState
class, remove theappBar
property from its Scaffold
Widget build(BuildContext context) {
...
return Scaffold(
// <- Comment out this appBar
// appBar: AppBar(
// leading: appState.selectedBook != null
// ? IconButton(
// icon: Icon(Icons.arrow_back),
// onPressed: () => _routerDelegate.popRoute(),
// )
// : null,
// ),
...
// Keep the rest part the same
);
}
- Add the
appBar
into each screens'Scaffold
, like this
class BooksListScreen extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(), // <- This is the key
...
);
}
}
Hope this makes sense
Thanks @xmkevinchen !!!!
The state of the home list not get saved when scrolling down the list and switching to the settings page and navigate to home again, the list scroll offset gets reset.
Does anyone know how to fix this?
import 'package:flutter/material.dart';
void main() {
runApp(NestedRouterDemo());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class NestedRouterDemo extends StatefulWidget {
@override
_NestedRouterDemoState createState() => _NestedRouterDemoState();
}
class _NestedRouterDemoState extends State<NestedRouterDemo> {
BookRouterDelegate _routerDelegate = BookRouterDelegate();
BookRouteInformationParser _routeInformationParser =
BookRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
}
}
class BooksAppState extends ChangeNotifier {
int _selectedIndex;
Book _selectedBook;
final List<Book> books = [
Book('Book 1', 'Ray Bradbury'),
Book('Book 2', 'Ray Bradbury'),
Book('Book 3', 'Ray Bradbury'),
Book('Book 4', 'Ray Bradbury'),
Book('Book 5', 'Ray Bradbury'),
Book('Book 6', 'Ray Bradbury'),
Book('Book 7', 'Ray Bradbury'),
Book('Book 8', 'Ray Bradbury'),
Book('Book 9', 'Ray Bradbury'),
Book('Book 10', 'Ray Bradbury'),
Book('Book 11', 'Ray Bradbury'),
Book('Book 12', 'Ray Bradbury'),
Book('Book 13', 'Ray Bradbury'),
Book('Book 14', 'Ray Bradbury'),
Book('Book 15', 'Ray Bradbury'),
Book('Book 16', 'Ray Bradbury'),
Book('Book 17', 'Ray Bradbury'),
Book('Book 18', 'Ray Bradbury'),
Book('Book 19', 'Ray Bradbury'),
Book('Book 20', 'Ray Bradbury'),
Book('Book 21', 'Ray Bradbury'),
Book('Book 22', 'Ray Bradbury'),
Book('Book 23', 'Ray Bradbury'),
Book('Book 24', 'Ray Bradbury'),
Book('Book 25', 'Ray Bradbury'),
Book('Book 26', 'Ray Bradbury'),
Book('Book 27', 'Ray Bradbury'),
Book('Book 28', 'Ray Bradbury'),
Book('Book 29', 'Ray Bradbury'),
Book('Book 30', 'Ray Bradbury'),
Book('Book 31', 'Ray Bradbury'),
Book('Book 32', 'Ray Bradbury'),
Book('Book 33', 'Ray Bradbury'),
Book('Book 34', 'Ray Bradbury'),
Book('Book 35', 'Ray Bradbury'),
Book('Book 36', 'Ray Bradbury'),
Book('Book 37', 'Ray Bradbury'),
Book('Book 38', 'Ray Bradbury'),
Book('Book 39', 'Ray Bradbury'),
Book('Book 40', 'Ray Bradbury'),
Book('Book 41', 'Ray Bradbury'),
Book('Book 42', 'Ray Bradbury'),
Book('Book 43', 'Ray Bradbury'),
];
BooksAppState() : _selectedIndex = 0;
int get selectedIndex => _selectedIndex;
set selectedIndex(int idx) {
_selectedIndex = idx;
if (_selectedIndex == 1) {
// Remove this line if you want to keep the selected book when navigating
// between "settings" and "home" which book was selected when Settings is
// tapped.
// selectedBook = null;
}
notifyListeners();
}
Book get selectedBook => _selectedBook;
set selectedBook(Book book) {
_selectedBook = book;
notifyListeners();
}
int getSelectedBookById() {
if (!books.contains(_selectedBook)) return 0;
return books.indexOf(_selectedBook);
}
void setSelectedBookById(int id) {
if (id < 0 || id > books.length - 1) {
return;
}
_selectedBook = books[id];
notifyListeners();
}
}
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
@override
Future<BookRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'settings') {
return BooksSettingsPath();
} else {
if (uri.pathSegments.length >= 2) {
if (uri.pathSegments[0] == 'book') {
return BooksDetailsPath(int.tryParse(uri.pathSegments[1]));
}
}
return BooksListPath();
}
}
@override
RouteInformation restoreRouteInformation(BookRoutePath configuration) {
if (configuration is BooksListPath) {
return RouteInformation(location: '/home');
}
if (configuration is BooksSettingsPath) {
return RouteInformation(location: '/settings');
}
if (configuration is BooksDetailsPath) {
return RouteInformation(location: '/book/${configuration.id}');
}
return null;
}
}
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
BooksAppState appState = BooksAppState();
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
appState.addListener(notifyListeners);
}
BookRoutePath get currentConfiguration {
if (appState.selectedIndex == 1) {
return BooksSettingsPath();
} else {
if (appState.selectedBook == null) {
return BooksListPath();
} else {
return BooksDetailsPath(appState.getSelectedBookById());
}
}
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
child: AppShell(appState: appState),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
if (appState.selectedBook != null) {
appState.selectedBook = null;
}
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath path) async {
if (path is BooksListPath) {
appState.selectedIndex = 0;
appState.selectedBook = null;
} else if (path is BooksSettingsPath) {
appState.selectedIndex = 1;
} else if (path is BooksDetailsPath) {
appState.setSelectedBookById(path.id);
}
}
}
// Routes
abstract class BookRoutePath {}
class BooksListPath extends BookRoutePath {}
class BooksSettingsPath extends BookRoutePath {}
class BooksDetailsPath extends BookRoutePath {
final int id;
BooksDetailsPath(this.id);
}
// Widget that contains the AdaptiveNavigationScaffold
class AppShell extends StatefulWidget {
final BooksAppState appState;
AppShell({
@required this.appState,
});
@override
_AppShellState createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
InnerRouterDelegate _routerDelegate;
ChildBackButtonDispatcher _backButtonDispatcher;
void initState() {
super.initState();
_routerDelegate = InnerRouterDelegate(widget.appState);
}
@override
void didUpdateWidget(covariant AppShell oldWidget) {
super.didUpdateWidget(oldWidget);
_routerDelegate.appState = widget.appState;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Defer back button dispatching to the child router
_backButtonDispatcher = Router.of(context)
.backButtonDispatcher
.createChildBackButtonDispatcher();
}
@override
Widget build(BuildContext context) {
var appState = widget.appState;
// Claim priority, If there are parallel sub router, you will need
// to pick which one should take priority;
_backButtonDispatcher.takePriority();
return Scaffold(
appBar: AppBar(),
body: Router(
routerDelegate: _routerDelegate,
backButtonDispatcher: _backButtonDispatcher,
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(
icon: Icon(Icons.settings), label: 'Settings'),
],
currentIndex: appState.selectedIndex,
onTap: (newIndex) {
appState.selectedIndex = newIndex;
},
),
);
}
}
class InnerRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
BooksAppState get appState => _appState;
BooksAppState _appState;
set appState(BooksAppState value) {
if (value == _appState) {
return;
}
_appState = value;
notifyListeners();
}
InnerRouterDelegate(this._appState);
FadeAnimationPage listPage;
FadeAnimationPage getList() {
if (listPage == null)
listPage = FadeAnimationPage(
child: BooksListScreen(
books: appState.books,
onTapped: _handleBookTapped,
),
key: ValueKey('BooksListPage'),
);
return listPage;
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
if (appState.selectedIndex == 0) ...[
getList(),
if (appState.selectedBook != null)
MaterialPage(
key: ValueKey(appState.selectedBook),
child: BookDetailsScreen(book: appState.selectedBook),
),
] else
FadeAnimationPage(
child: SettingsScreen(),
key: ValueKey('SettingsPage'),
),
],
onPopPage: (route, result) {
appState.selectedBook = null;
notifyListeners();
return route.didPop(result);
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath path) async {
// This is not required for inner router delegate because it does not
// parse route
assert(false);
}
void _handleBookTapped(Book book) {
appState.selectedBook = book;
notifyListeners();
}
}
class FadeAnimationPage extends Page {
final Widget child;
FadeAnimationPage({Key key, this.child}) : super(key: key);
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
pageBuilder: (context, animation, animation2) {
var curveTween = CurveTween(curve: Curves.easeIn);
return FadeTransition(
opacity: animation.drive(curveTween),
child: child,
);
},
);
}
}
// Screens
class BooksListScreen extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book> onTapped;
BooksListScreen({
@required this.books,
@required this.onTapped,
}));
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => onTapped(book),
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen({
@required this.book,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlatButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('Back'),
),
if (book != null) ...[
Text(book.title, style: Theme.of(context).textTheme.headline6),
Text(book.author, style: Theme.of(context).textTheme.subtitle1),
],
],
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Settings screen'),
),
);
}
}
Upgrade to flutter sdk >= 2.12
import 'package:flutter/material.dart';
void main() {
runApp(NestedRouterDemo());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class NestedRouterDemo extends StatefulWidget {
@override
_NestedRouterDemoState createState() => _NestedRouterDemoState();
}
class _NestedRouterDemoState extends State<NestedRouterDemo> {
BookRouterDelegate _routerDelegate = BookRouterDelegate();
BookRouteInformationParser _routeInformationParser = BookRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
}
}
class BooksAppState extends ChangeNotifier {
int _selectedIndex;
Book? _selectedBook;
final List<Book> books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
BooksAppState() : _selectedIndex = 0;
int get selectedIndex => _selectedIndex;
set selectedIndex(int idx) {
_selectedIndex = idx;
notifyListeners();
}
Book? get selectedBook => _selectedBook;
set selectedBook(Book? book) {
_selectedBook = book;
notifyListeners();
}
int? getSelectedBookById() {
if (_selectedBook == null || !books.contains(_selectedBook)) return null;
return books.indexOf(_selectedBook!);
}
void setSelectedBookById(int id) {
if (id < 0 || id > books.length - 1) {
return;
}
_selectedBook = books[id];
notifyListeners();
}
}
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
@override
Future<BookRoutePath> parseRouteInformation(RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location ?? '');
if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'settings') {
return BooksSettingsPath();
} else {
if (uri.pathSegments.length >= 2) {
if (uri.pathSegments[0] == 'book') {
return BooksDetailsPath(int.tryParse(uri.pathSegments[1]));
}
}
return BooksListPath();
}
}
@override
RouteInformation restoreRouteInformation(BookRoutePath configuration) {
if (configuration is BooksListPath) {
return RouteInformation(location: '/home');
}
if (configuration is BooksSettingsPath) {
return RouteInformation(location: '/settings');
}
if (configuration is BooksDetailsPath) {
return RouteInformation(location: '/book/${configuration.id}');
}
return RouteInformation();
}
}
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
BooksAppState appState = BooksAppState();
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
appState.addListener(notifyListeners);
}
BookRoutePath get currentConfiguration {
if (appState.selectedIndex == 1) {
return BooksSettingsPath();
} else {
if (appState.selectedBook == null) {
return BooksListPath();
} else {
return BooksDetailsPath(appState.getSelectedBookById());
}
}
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
child: AppShell(appState: appState),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
if (appState.selectedBook != null) {
appState.selectedBook = null;
}
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath path) async {
if (path is BooksListPath) {
appState.selectedIndex = 0;
appState.selectedBook = null;
} else if (path is BooksSettingsPath) {
appState.selectedIndex = 1;
} else if (path is BooksDetailsPath && path.id != null) {
appState.setSelectedBookById(path.id!);
}
}
}
// Routes
abstract class BookRoutePath {}
class BooksListPath extends BookRoutePath {}
class BooksSettingsPath extends BookRoutePath {}
class BooksDetailsPath extends BookRoutePath {
final int? id;
BooksDetailsPath(this.id);
}
// Widget that contains the AdaptiveNavigationScaffold
class AppShell extends StatefulWidget {
final BooksAppState appState;
AppShell({
required this.appState,
});
@override
_AppShellState createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
late InnerRouterDelegate _routerDelegate;
late ChildBackButtonDispatcher _backButtonDispatcher;
void initState() {
super.initState();
_routerDelegate = InnerRouterDelegate(widget.appState);
}
@override
void didUpdateWidget(covariant AppShell oldWidget) {
super.didUpdateWidget(oldWidget);
_routerDelegate.appState = widget.appState;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Defer back button dispatching to the child router
_backButtonDispatcher = Router.of(context).backButtonDispatcher!.createChildBackButtonDispatcher();
}
@override
Widget build(BuildContext context) {
var appState = widget.appState;
// Claim priority, If there are parallel sub router, you will need
// to pick which one should take priority;
_backButtonDispatcher.takePriority();
return Scaffold(
appBar: AppBar(),
body: Router(
routerDelegate: _routerDelegate,
backButtonDispatcher: _backButtonDispatcher,
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'),
],
currentIndex: appState.selectedIndex,
onTap: (newIndex) {
appState.selectedIndex = newIndex;
},
),
);
}
}
class InnerRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
BooksAppState get appState => _appState;
BooksAppState _appState;
set appState(BooksAppState value) {
if (value == _appState) {
return;
}
_appState = value;
notifyListeners();
}
InnerRouterDelegate(this._appState);
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
if (appState.selectedIndex == 0) ...[
FadeAnimationPage(
child: BooksListScreen(
books: appState.books,
onTapped: _handleBookTapped,
),
key: ValueKey('BooksListPage'),
),
if (appState.selectedBook != null)
MaterialPage(
key: ValueKey(appState.selectedBook),
child: BookDetailsScreen(book: appState.selectedBook!),
),
] else
FadeAnimationPage(
child: SettingsScreen(),
key: ValueKey('SettingsPage'),
),
],
onPopPage: (route, result) {
appState.selectedBook = null;
notifyListeners();
return route.didPop(result);
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath path) async {
// This is not required for inner router delegate because it does not
// parse route
assert(false);
}
void _handleBookTapped(Book book) {
appState.selectedBook = book;
notifyListeners();
}
}
class FadeAnimationPage extends Page {
final Widget child;
FadeAnimationPage({required LocalKey key, required this.child}) : super(key: key);
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
pageBuilder: (context, animation, animation2) {
var curveTween = CurveTween(curve: Curves.easeIn);
return FadeTransition(
opacity: animation.drive(curveTween),
child: child,
);
},
);
}
}
// Screens
class BooksListScreen extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book> onTapped;
BooksListScreen({
required this.books,
required this.onTapped,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => onTapped(book),
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen({
required this.book,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('Back'),
),
Text(book.title, style: Theme.of(context).textTheme.headline6),
Text(book.author, style: Theme.of(context).textTheme.subtitle1),
],
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Settings screen'),
),
);
}
}
If you are retrieving a list of books from Firestore (or some API), how do you refresh a list of books listed on BooksListScreen
when coming back from BookDetailsScreen
? Let's say you add a new book on BookDetailsScreen
, then you probably want to show the new book added to the list of books when coming back to BooksListScreen
.
Upgrade to Flutter SDK >= 3.0.0
import 'package:flutter/material.dart';
void main() {
runApp(const NestedRouterDemo());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class NestedRouterDemo extends StatefulWidget {
const NestedRouterDemo({Key? key}) : super(key: key);
@override
State<NestedRouterDemo> createState() => _NestedRouterDemoState();
}
class _NestedRouterDemoState extends State<NestedRouterDemo> {
final BookRouterDelegate _routerDelegate = BookRouterDelegate();
final BookRouteInformationParser _routeInformationParser =
BookRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
}
}
class BooksAppState extends ChangeNotifier {
int _selectedIndex;
Book? _selectedBook;
final List<Book> books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
BooksAppState() : _selectedIndex = 0;
int get selectedIndex => _selectedIndex;
set selectedIndex(int idx) {
_selectedIndex = idx;
notifyListeners();
}
Book? get selectedBook => _selectedBook;
set selectedBook(Book? book) {
_selectedBook = book;
notifyListeners();
}
int? getSelectedBookById() {
if (_selectedBook == null || !books.contains(_selectedBook)) return null;
return books.indexOf(_selectedBook!);
}
void setSelectedBookById(int id) {
if (id < 0 || id > books.length - 1) {
return;
}
_selectedBook = books[id];
notifyListeners();
}
}
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
@override
Future<BookRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location ?? '');
if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'settings') {
return BooksSettingsPath();
} else {
if (uri.pathSegments.length >= 2) {
if (uri.pathSegments[0] == 'book') {
return BooksDetailsPath(int.tryParse(uri.pathSegments[1]));
}
}
return BooksListPath();
}
}
@override
RouteInformation restoreRouteInformation(BookRoutePath configuration) {
if (configuration is BooksListPath) {
return const RouteInformation(location: '/home');
}
if (configuration is BooksSettingsPath) {
return const RouteInformation(location: '/settings');
}
if (configuration is BooksDetailsPath) {
return RouteInformation(location: '/book/${configuration.id}');
}
return const RouteInformation();
}
}
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey;
BooksAppState appState = BooksAppState();
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
appState.addListener(notifyListeners);
}
@override
BookRoutePath get currentConfiguration {
if (appState.selectedIndex == 1) {
return BooksSettingsPath();
} else {
if (appState.selectedBook == null) {
return BooksListPath();
} else {
return BooksDetailsPath(appState.getSelectedBookById());
}
}
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
child: AppShell(appState: appState),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
if (appState.selectedBook != null) {
appState.selectedBook = null;
}
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath configuration) async {
if (configuration is BooksListPath) {
appState.selectedIndex = 0;
appState.selectedBook = null;
} else if (configuration is BooksSettingsPath) {
appState.selectedIndex = 1;
} else if (configuration is BooksDetailsPath && configuration.id != null) {
appState.setSelectedBookById(configuration.id!);
}
}
}
// Routes
abstract class BookRoutePath {}
class BooksListPath extends BookRoutePath {}
class BooksSettingsPath extends BookRoutePath {}
class BooksDetailsPath extends BookRoutePath {
final int? id;
BooksDetailsPath(this.id);
}
// Widget that contains the AdaptiveNavigationScaffold
class AppShell extends StatefulWidget {
const AppShell({
Key? key,
required this.appState,
}) : super(key: key);
final BooksAppState appState;
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
late InnerRouterDelegate _routerDelegate;
late ChildBackButtonDispatcher _backButtonDispatcher;
@override
void initState() {
super.initState();
_routerDelegate = InnerRouterDelegate(widget.appState);
}
@override
void didUpdateWidget(covariant AppShell oldWidget) {
super.didUpdateWidget(oldWidget);
_routerDelegate.appState = widget.appState;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Defer back button dispatching to the child router
_backButtonDispatcher = Router.of(context)
.backButtonDispatcher!
.createChildBackButtonDispatcher();
}
@override
Widget build(BuildContext context) {
var appState = widget.appState;
// Claim priority, If there are parallel sub router, you will need
// to pick which one should take priority;
_backButtonDispatcher.takePriority();
return Scaffold(
appBar: AppBar(),
body: Router(
routerDelegate: _routerDelegate,
backButtonDispatcher: _backButtonDispatcher,
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(
icon: Icon(Icons.settings), label: 'Settings'),
],
currentIndex: appState.selectedIndex,
onTap: (newIndex) {
appState.selectedIndex = newIndex;
},
),
);
}
}
class InnerRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
BooksAppState get appState => _appState;
BooksAppState _appState;
set appState(BooksAppState value) {
if (value == _appState) {
return;
}
_appState = value;
notifyListeners();
}
InnerRouterDelegate(this._appState);
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
if (appState.selectedIndex == 0) ...[
FadeAnimationPage(
child: BooksListScreen(
books: appState.books,
onTapped: _handleBookTapped,
),
key: const ValueKey('BooksListPage'),
),
if (appState.selectedBook != null)
MaterialPage(
key: ValueKey(appState.selectedBook),
child: BookDetailsScreen(book: appState.selectedBook!),
),
] else
const FadeAnimationPage(
child: SettingsScreen(),
key: ValueKey('SettingsPage'),
),
],
onPopPage: (route, result) {
appState.selectedBook = null;
notifyListeners();
return route.didPop(result);
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath configuration) async {
// This is not required for inner router delegate because it does not
// parse route
assert(false);
}
void _handleBookTapped(Book book) {
appState.selectedBook = book;
notifyListeners();
}
}
class FadeAnimationPage extends Page {
const FadeAnimationPage({required LocalKey key, required this.child})
: super(key: key);
final Widget child;
@override
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
pageBuilder: (context, animation, animation2) {
var curveTween = CurveTween(curve: Curves.easeIn);
return FadeTransition(
opacity: animation.drive(curveTween),
child: child,
);
},
);
}
}
// Screens
class BooksListScreen extends StatelessWidget {
const BooksListScreen({
Key? key,
required this.books,
required this.onTapped,
}) : super(key: key);
final List<Book> books;
final ValueChanged<Book> onTapped;
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => onTapped(book),
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
const BookDetailsScreen({Key? key, required this.book}) : super(key: key);
final Book book;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Back'),
),
Text(book.title, style: Theme.of(context).textTheme.headline6),
Text(book.author, style: Theme.of(context).textTheme.subtitle1),
],
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
const SettingsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('Settings screen'),
),
);
}
}
- Why calling
notifyListeners();
in
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
if (appState.selectedBook != null) {
appState.selectedBook = null;
}
notifyListeners();
return true;
},
?
If we change appState.selectedBook = null;
then listeners will be called by the appState.addListener(notifyListeners);
isn't they?
- Why do we do
_routerDelegate.appState = widget.appState
in:
@override
void didUpdateWidget(covariant AppShell oldWidget) {
super.didUpdateWidget(oldWidget);
_routerDelegate.appState = widget.appState;
}
?
isn't appState
passed to InnerRouterDelegate
by reference, and so all the changes in the internals are reflected there?
- Why we do
appState.selectedBook = null;
in:
onPopPage: (route, result) {
appState.selectedBook = null;
notifyListeners();
return route.didPop(result);
}
instead of calling _handleBookTapped(null)
that we already have?
- Why
AppShell
isStatefulWidget
since it doesn't hold any state and
_backButtonDispatcher = Router.of(context)
.backButtonDispatcher!
.createChildBackButtonDispatcher();
could be done in 'build` method?
Anyway - this seem to work exactly the same:
import 'package:flutter/material.dart';
void main() {
runApp(const NestedRouterDemo());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class NestedRouterDemo extends StatefulWidget {
const NestedRouterDemo({Key? key}) : super(key: key);
@override
State<NestedRouterDemo> createState() => _NestedRouterDemoState();
}
class _NestedRouterDemoState extends State<NestedRouterDemo> {
final BookRouterDelegate _routerDelegate = BookRouterDelegate();
final BookRouteInformationParser _routeInformationParser =
BookRouteInformationParser();
@override
Widget build(BuildContext context) => MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
}
class BooksAppState extends ChangeNotifier {
int _selectedIndex;
Book? _selectedBook;
final List<Book> books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
BooksAppState() : _selectedIndex = 0;
int get selectedIndex => _selectedIndex;
set selectedIndex(int idx) {
_selectedIndex = idx;
notifyListeners();
}
Book? get selectedBook => _selectedBook;
set selectedBook(Book? book) {
_selectedBook = book;
notifyListeners();
}
int? getSelectedBookById() {
if (_selectedBook == null || !books.contains(_selectedBook)) {
return null;
}
return books.indexOf(_selectedBook!);
}
void setSelectedBookById(int id) {
if (id < 0 || id > books.length - 1) {
return;
}
_selectedBook = books[id];
notifyListeners();
}
}
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
@override
Future<BookRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location ?? '');
if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'settings') {
return BooksSettingsPath();
} else {
if (uri.pathSegments.length >= 2) {
if (uri.pathSegments[0] == 'book') {
return BooksDetailsPath(int.tryParse(uri.pathSegments[1]));
}
}
return BooksListPath();
}
}
@override
RouteInformation restoreRouteInformation(BookRoutePath configuration) {
if (configuration is BooksListPath) {
return const RouteInformation(location: '/home');
}
if (configuration is BooksSettingsPath) {
return const RouteInformation(location: '/settings');
}
if (configuration is BooksDetailsPath) {
return RouteInformation(location: '/book/${configuration.id}');
}
return const RouteInformation();
}
}
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey;
BooksAppState appState = BooksAppState();
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
appState.addListener(notifyListeners);
}
@override
BookRoutePath get currentConfiguration {
if (appState.selectedIndex == 1) {
return BooksSettingsPath();
} else {
if (appState.selectedBook == null) {
return BooksListPath();
} else {
return BooksDetailsPath(appState.getSelectedBookById());
}
}
}
@override
Widget build(BuildContext context) {
print('BookRouterDelegate#build');
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
child: AppShell(appState: appState),
),
],
onPopPage: (route, result) {
print('BookRouterDelegate#onPopPage');
if (!route.didPop(result)) {
return false;
}
if (appState.selectedBook != null) {
appState.selectedBook = null;
}
return true;
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath configuration) async {
if (configuration is BooksListPath) {
appState.selectedIndex = 0;
appState.selectedBook = null;
} else if (configuration is BooksSettingsPath) {
appState.selectedIndex = 1;
} else if (configuration is BooksDetailsPath && configuration.id != null) {
appState.setSelectedBookById(configuration.id!);
}
}
}
// Routes
abstract class BookRoutePath {}
class BooksListPath extends BookRoutePath {}
class BooksSettingsPath extends BookRoutePath {}
class BooksDetailsPath extends BookRoutePath {
final int? id;
BooksDetailsPath(this.id);
}
// Widget that contains the AdaptiveNavigationScaffold
class AppShell extends StatelessWidget {
AppShell({
Key? key,
required this.appState,
}) : _routerDelegate = InnerRouterDelegate(appState), super(key: key);
final BooksAppState appState;
final InnerRouterDelegate _routerDelegate;
@override
Widget build(BuildContext context) {
// Defer back button dispatching to the child router
final ChildBackButtonDispatcher _backButtonDispatcher = Router.of(context)
.backButtonDispatcher!
.createChildBackButtonDispatcher();
// Claim priority, If there are parallel sub router, you will need
// to pick which one should take priority;
_backButtonDispatcher.takePriority();
return Scaffold(
appBar: AppBar(),
body: SafeArea(
top: false,
child: Router(
routerDelegate: _routerDelegate,
backButtonDispatcher: _backButtonDispatcher,
),
),
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(
icon: Icon(Icons.settings), label: 'Settings'),
],
currentIndex: appState.selectedIndex,
onTap: (newIndex) {
appState.selectedIndex = newIndex;
},
),
);
}
}
class InnerRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
BooksAppState appState;
InnerRouterDelegate(this.appState) {
//not really needed, since InnerRouterDelegate is rebuild anyway everytime when BookRouterDelegate is rebuild
//appState.addListener(notifyListeners);
}
@override
Widget build(BuildContext context) {
print('InnerRouterDelegate#build');
return Navigator(
key: navigatorKey,
pages: [
if (appState.selectedIndex == 0) ...[
FadeAnimationPage(
child: BooksListScreen(
books: appState.books,
onTapped: _handleBookTapped,
),
key: const ValueKey('BooksListPage'),
),
if (appState.selectedBook != null)
MaterialPage(
key: ValueKey(appState.selectedBook),
child: BookDetailsScreen(book: appState.selectedBook!),
),
] else
const FadeAnimationPage(
child: SettingsScreen(),
key: ValueKey('SettingsPage'),
),
],
onPopPage: (route, result) {
print('InnerRouterDelegate#onPopPage');
_handleBookTapped(null);
return route.didPop(result);
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath configuration) async {
// This is not required for inner router delegate because it does not parse route
assert(false);
}
void _handleBookTapped(Book? book) {
appState.selectedBook = book;
}
}
class FadeAnimationPage extends Page {
const FadeAnimationPage({required LocalKey key, required this.child})
: super(key: key);
final Widget child;
@override
Route createRoute(BuildContext context) {
return PageRouteBuilder(
settings: this,
pageBuilder: (context, animation, animation2) {
var curveTween = CurveTween(curve: Curves.easeIn);
return FadeTransition(
opacity: animation.drive(curveTween),
child: child,
);
},
);
}
}
// Screens
class BooksListScreen extends StatelessWidget {
const BooksListScreen({
Key? key,
required this.books,
required this.onTapped,
}) : super(key: key);
final List<Book> books;
final ValueChanged<Book> onTapped;
@override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () {
onTapped(book);
},
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
const BookDetailsScreen({Key? key, required this.book}) : super(key: key);
final Book book;
@override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(), //if we want the AppBar back button to appear
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Back'),
),
Text(book.title, style: Theme.of(context).textTheme.titleLarge),
Text(book.author, style: Theme.of(context).textTheme.titleMedium),
],
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
const SettingsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => const Scaffold(
// appBar: AppBar(),
body: Center(
child: Text('Settings screen'),
),
);
}
I have a question that has been asked before, about the general Router 2.0: flutter/flutter#45938 (comment) . Specifically about this nested router example, how are we managing the states of these two routers? The solution explained here: flutter/flutter#45938 (comment) is great, but doesn't seem to work for this two-router solution? Am I missing something? Additionally, similarly to @lukemadera above, using Provider for BookAppState doesn't rebuild the route.