Skip to content

Instantly share code, notes, and snippets.

@johnpryan
Last active Sep 17, 2021
Embed
What would you like to do?
Declarative Navigation ex. 9 - Navigation Rail + Router + Animations
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'),
),
);
}
}
@xwpony

This comment has been minimized.

Copy link

@xwpony xwpony commented Oct 7, 2020

I had an issue running this gist.
If url is on /settings and manually set url to /book/1, url would still stay at /settings.

Flutter (Channel dev, 1.23.0-7.0.pre, on Mac OS X 10.15.6 19G2021 x86_64, locale en-US)
Engine revision 3a73d073c8
Dart version 2.11.0 (build 2.11.0-161.0.dev)
@grinder15

This comment has been minimized.

Copy link

@grinder15 grinder15 commented Oct 9, 2020

Tried this example.
Url not changing when u navigate.
I can type the url and it goes to specific route but the url stays at root.

[√] Flutter (Channel beta, 1.22.1, on Microsoft Windows [Version 10.0.18363.1082], locale en-PH)
    • Flutter version 1.22.1 at C:\src\flutter
    • Framework revision f30b7f4db9 (29 hours ago), 2020-10-08 10:06:30 -0700
    • Engine revision 75bef9f6c8
    • Dart version 2.10.1


[√] Android toolchain - develop for Android devices (Android SDK version 30.0.0-rc1)
    • Android SDK at C:\Users\Katrina\AppData\Local\Android\Sdk
    • Platform android-30, build-tools 30.0.0-rc1
    • Java binary at: C:\Program Files\Android\Android Studio\jre\bin\java
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)
    • All Android licenses accepted.

[√] Chrome - develop for the web
    • Chrome at C:\Program Files (x86)\Google\Chrome\Application\chrome.exe

[√] Android Studio (version 4.0)
    • Android Studio at C:\Program Files\Android\Android Studio
    • Flutter plugin version 50.0.1
    • Dart plugin version 193.7547
    • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b01)

[√] VS Code (version 1.50.0)
    • VS Code at C:\Users\Katrina\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension version 3.15.0

[√] Connected device (3 available)
    • Web Server (web) • web-server • web-javascript • Flutter Tools
    • Chrome (web)     • chrome     • web-javascript • Google Chrome 85.0.4183.121
    • Edge (web)       • edge       • web-javascript • Microsoft Edge 85.0.564.68
@sgoroshko

This comment has been minimized.

Copy link

@sgoroshko sgoroshko commented Oct 9, 2020

you must checkout to master branch
flutter channel master

@RedTech64

This comment has been minimized.

Copy link

@RedTech64 RedTech64 commented Oct 10, 2020

I'm on the master branch and I also have the problem where changing the link from settings to something like book/1 doesn't work.

@Milad-Akarie

This comment has been minimized.

Copy link

@Milad-Akarie Milad-Akarie commented Oct 12, 2020

try setting a path name to MaterialPage

 MaterialPage(
              name: '/book/${book_id}',
              key: ValueKey(appState.selectedBook),
              child: BookDetailsScreen(book: appState.selectedBook),
            ),
@giorgio79

This comment has been minimized.

Copy link

@giorgio79 giorgio79 commented Oct 21, 2020

The state is not saved, eg if I select one of the books, then go to settings and back to books, I am back at the starting screen.

@giorgio79

This comment has been minimized.

Copy link

@giorgio79 giorgio79 commented Oct 21, 2020

@leonardarnold

This comment has been minimized.

Copy link

@leonardarnold leonardarnold commented Oct 21, 2020

The state is not saved, eg if I select one of the books, then go to settings and back to books, I am back at the starting screen.

Please read the line 52-52 and uncomment the line 55. This should make it work as you expect

This example works! https://github.com/Mr-Pepe/nested-navigation/blob/master/lib/main.dart
You can navigate to subpages of a tab, and switch to another one, then back.
Taken from https://medium.com/@Mr_Pepe/nested-navigation-with-a-bottom-navigation-bar-using-flutter-d3c5086fbcdc

This is out of context because the gist here is about navigator 2.0 and this example you mentioned is navigator 1
In the end you can decide which way you want to go as both works : - )

@giorgio79

This comment has been minimized.

Copy link

@giorgio79 giorgio79 commented Oct 21, 2020

Nice! Thanks @leonardarnold Indeed it works!
I will try this Nav 2 as I would eventually like Web and other builds, so trying to stick with official Flutter ....
PS I do notice this solution is 400 lines of code, while the nav 1 is 200. Hopefully, we can abstract away most of the stuff, and just plug in the paths eventually like good old html sites...

@giorgio79

This comment has been minimized.

Copy link

@giorgio79 giorgio79 commented Oct 23, 2020

PS could we get an example where routes are not list items from a query, but perhaps predefined navigation steps, such as a checkout process.
Eg user confirms selection
user enters address
user payment

Essentially, it comes down to state management concepts, and the best solution still seems to be the BehaviorSubject solution described here https://fireship.io/lessons/flutter-state-management-guide/ therefore the entire navigation is essentially a stream.
The best!

@pcmushthaq

This comment has been minimized.

Copy link

@pcmushthaq pcmushthaq commented Oct 24, 2020

I had an issue running this gist.
If url is on /settings and manually set url to /book/1, url would still stay at /settings.

Flutter (Channel dev, 1.23.0-7.0.pre, on Mac OS X 10.15.6 19G2021 x86_64, locale en-US)
Engine revision 3a73d073c8
Dart version 2.11.0 (build 2.11.0-161.0.dev)

I have the same problem.

@giorgio79

This comment has been minimized.

Copy link

@giorgio79 giorgio79 commented Oct 28, 2020

Re architecture, could we get sg like this instead of the super heavy logical labyrinth above? Lets keep it simple guys please

TabbedBarNav = [
Books(),
Coffees(),
];

Route Books = [
BookSettings();
BookSelect();
BookDetail;
];

Route Coffees  = [
CoffeeSettings();
CoffeeSelect();
CoffeeDetail;
];

functionToSetSelectedTab () {
Mycustom-logic-to-decide-which-page-loads
}

@dkbast

This comment has been minimized.

Copy link

@dkbast dkbast commented Oct 31, 2020

@xwpony @giorgio79 you just need to update the selected index in setNewRoutePath

  @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.selectedIndex = 0; // This was missing!
      appState.setSelectedBookById(path.id); 
    }
  }
}
@by90

This comment has been minimized.

Copy link

@by90 by90 commented Nov 3, 2020

I've just modify your code,the simple way
class BookRoutePath {
String url;
BookRoutePath(this.url);
}
so we don't need so many state for route,we could navgator like react router

all worked,but back button in detail screen couldn't.
can you tell me what wrong with me?
when we use back button in browser,it worked....

the whole code here:

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 {
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 {
String _appUrl;

final List books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];

BooksAppState() : _appUrl = '/';

String get appUrl => _appUrl;

set appUrl(String idx) {
_appUrl = idx;
notifyListeners();
}

Book getBookById(int id) {
if (id < 0 || id > books.length - 1) {
return null;
}
return books[id];
}

int getIdByBook(Book book) {
if (!books.contains(book)) return 0;
return books.indexOf(book);
}

int getIdFromUrl(String path) {
final uri = Uri.parse(path);
return int.tryParse(uri.pathSegments[1]);
}

// Book get selectedBook => _selectedBook;

// set selectedBook(Book book) {
// _selectedBook = book;
// notifyListeners();
// }

// int getSelectedBookById(int id) {
// 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 {
@override
Future parseRouteInformation(
RouteInformation routeInformation) async {
//final uri = Uri.parse(routeInformation.location);
return (BookRoutePath(routeInformation.location));
}

@override
RouteInformation restoreRouteInformation(BookRoutePath configuration) {
//if (configuration.url == null) return null;
return RouteInformation(location: configuration.url);
}
}

class BookRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
final GlobalKey navigatorKey;

BooksAppState appState = BooksAppState();

BookRouterDelegate() : navigatorKey = GlobalKey() {
appState.addListener(notifyListeners);
}

BookRoutePath get currentConfiguration {
return BookRoutePath(appState.appUrl);
}

@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
child: AppShell(appState: appState),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}

    //这里通过设定selectredBook,从而显示列表?
    // if (appState.selectedBook != null) {
    //   appState.selectedBook = null;
    // }
    print('onPopPage,route.settings.name=${route.settings.name}');
    appState.appUrl = route.settings.name;
    notifyListeners();
    return true;
  },
);

}

@override
Future setNewRoutePath(BookRoutePath path) async {
print('setNewRoutePath:path=${path.url}');
appState.appUrl = path.url; //这里来设定状态,但是我们已经notify了;
// 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
class BookRoutePath {
String url;
BookRoutePath(this.url);
}

// Widget that contains the AdaptiveNavigationScaffold
class AppShell extends StatefulWidget {
final BooksAppState appState;

AppShell({
@required this.appState,
});

@override
_AppShellState createState() => _AppShellState();
}

class _AppShellState extends State {
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: 0, //appState.selectedIndex,
    onTap: (newIndex) {
      appState.appUrl = newIndex == 0 ? '/' : '/settings';
      //appState.selectedIndex = newIndex;
    },
  ),
);

}
}

class InnerRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
final GlobalKey navigatorKey = GlobalKey();
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.appUrl == '/')
MaterialPage(
child: BooksListScreen(
books: appState.books,
onTapped: _handleBookTapped,
),
key: ValueKey(appState.appUrl),
name: appState.appUrl),
if (appState.appUrl.startsWith('/post'))
MaterialPage(
name: appState.appUrl,
key: ValueKey(appState.appUrl),
child: BookDetailsScreen(
book: appState
.getBookById(appState.getIdFromUrl(appState.appUrl))),
),
if (appState.appUrl == '/settings')
MaterialPage(
name: appState.appUrl,
child: SettingsScreen(),
key: ValueKey(appState.appUrl),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
//appState.selectedBook = null;
print('onPopPage,route.settings.name=${route.settings.name}');
appState.appUrl = route.settings.name;
//notifyListeners();
return true;
},
);
}

@override
Future 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.appUrl = '/post/${appState.getIdByBook(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 books;
final ValueChanged 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'),
),
);
}
}

@enyo

This comment has been minimized.

Copy link

@enyo enyo commented Nov 4, 2020

It's a bit unclear to me, why a nested router is necessary here. Could you elaborate on this a bit? There's also the line where the dispatcher takes priority (_backButtonDispatcher.takePriority()). How would a parallel sub router work in this case? Thanks

@syfulin

This comment has been minimized.

Copy link

@syfulin syfulin commented Dec 8, 2020

What is special about ValueChanged<Book>? If I want to pass more than one argument to a function?

@lukemadera

This comment has been minimized.

Copy link

@lukemadera lukemadera commented Dec 8, 2020

How do we navigate to a named route (from another file / screen)? I've read that it is navigatorKey.currentState.pushNamed('/settings'); but that throws an error about navigatorKey. I see in this example in MaterialApp the navigatorKey is not being set - is that the issue? If so, how do we get it from the router and set it?

I also tried to update the BookAppState selectedIndex (which DOES work from within the router classes, but not from other classes and widgets in other files / screens) with Provider.of<BooksAppState>(context, listen: false).setSelectedIndex(1); but that doesn't rebuild the view to the new route.

@jonasjuni

This comment has been minimized.

Copy link

@jonasjuni jonasjuni commented Dec 27, 2020

Hi guys, I'm new to Flutter/Dart following this example I've tried to change the FlatButton pop to the AppBar in the parent route:
FROM:

class BookDetailsScreen extends StatelessWidget {
  @override
 Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
       children: [
            FlatButton(
              onPressed: () {
               Navigator.of(context).pop();
              },
              child: Text('Back'),
              ),
          ],
}

TO the parent AppShell state

class _AppShellState extends State<AppShell> {
  InnerRouterDelegate _routerDelegate;
  ChildBackButtonDispatcher _backButtonDispatcher;
   return Scaffold(
      appBar: AppBar(
        leading: appState.selectedBook != null
            ? IconButton(
                icon: Icon(Icons.arrow_back),
                onPressed: () => _routerDelegate.popRoute(),
              )
            : null,
      )

It's working, but I'm not sure if it's the best practice to use _routerDelegate.popRoute()

@enyo

This comment has been minimized.

Copy link

@enyo enyo commented Dec 28, 2020

@jonasjuni it's probably better to post this on stackoverflow. Please post the link here too after you've done that.

@jonasjuni

This comment has been minimized.

Copy link

@jonasjuni jonasjuni commented Dec 28, 2020

@jonasjuni it's probably better to post this on stackoverflow. Please post the link here too after you've done that.

Thanks mate! it is here https://stackoverflow.com/questions/65480003/flutter-navigator-v2-0-how-to-pop-a-child-nested-route

@tsx1453

This comment has been minimized.

Copy link

@tsx1453 tsx1453 commented Jan 11, 2021

I have a question, if i open a page that on the root level navigator,how can i close the page by back button? I have the problem that when i open some page in inner navigator, then open a root navigator page, i can't close the root navigator page until inner navigator has no more route

@stingrayx

This comment has been minimized.

Copy link

@stingrayx stingrayx commented Jan 16, 2021

I have a question to the example discussed above from Mr. Pepe https://github.com/Mr-Pepe/nested-navigation/blob/master/lib/main.dart

Is there a way to lazy load the navigator and their underlying pages, which are put on the IndexedStack?

@bkoznov

This comment has been minimized.

Copy link

@bkoznov bkoznov commented Feb 8, 2021

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.

@slovnicki

This comment has been minimized.

Copy link

@slovnicki slovnicki commented Feb 21, 2021

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: ...,
  ),
)
@xmkevinchen

This comment has been minimized.

Copy link

@xmkevinchen xmkevinchen commented Feb 28, 2021

@jonasjuni I've a tweak for adding the back button on the AppBar based on this sample code. Please check it out.

  1. In the _AppShellState class, remove the appBar 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
      
    );
  }
  1. 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

@jonasjuni

This comment has been minimized.

Copy link

@jonasjuni jonasjuni commented Feb 28, 2021

Thanks @xmkevinchen !!!!

@PlugFox

This comment has been minimized.

@ali-star

This comment has been minimized.

Copy link

@ali-star ali-star commented Apr 13, 2021

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'),
      ),
    );
  }
}
@dleurs

This comment has been minimized.

Copy link

@dleurs dleurs commented Aug 16, 2021

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'),
      ),
    );
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment