-
-
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'), | |
), | |
); | |
} | |
} |
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'),
),
);
}
Upgrade to flutter sdk >= 2.12