-
-
Save johnpryan/5ce79aee5b5f83cfababa97c9cf0a204 to your computer and use it in GitHub Desktop.
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(BooksApp()); | |
} | |
class Book { | |
final String title; | |
final String author; | |
Book(this.title, this.author); | |
} | |
class BooksApp extends StatefulWidget { | |
@override | |
State<StatefulWidget> createState() => _BooksAppState(); | |
} | |
class _BooksAppState extends State<BooksApp> { | |
BookRouterDelegate _routerDelegate = BookRouterDelegate(); | |
BookRouteInformationParser _routeInformationParser = | |
BookRouteInformationParser(); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp.router( | |
title: 'Books App', | |
routerDelegate: _routerDelegate, | |
routeInformationParser: _routeInformationParser, | |
); | |
} | |
} | |
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> { | |
@override | |
Future<BookRoutePath> parseRouteInformation( | |
RouteInformation routeInformation) async { | |
final uri = Uri.parse(routeInformation.location); | |
if (uri.pathSegments.length >= 2) { | |
var remaining = uri.pathSegments[1]; | |
return BookRoutePath.details(int.tryParse(remaining)); | |
} else { | |
return BookRoutePath.home(); | |
} | |
} | |
@override | |
RouteInformation restoreRouteInformation(BookRoutePath path) { | |
if (path.isHomePage) { | |
return RouteInformation(location: '/'); | |
} | |
if (path.isDetailsPage) { | |
return RouteInformation(location: '/book/${path.id}'); | |
} | |
return null; | |
} | |
} | |
class BookRouterDelegate extends RouterDelegate<BookRoutePath> | |
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> { | |
final GlobalKey<NavigatorState> navigatorKey; | |
Book _selectedBook; | |
List<Book> books = [ | |
Book('Stranger in a Strange Land', 'Robert A. Heinlein'), | |
Book('Foundation', 'Isaac Asimov'), | |
Book('Fahrenheit 451', 'Ray Bradbury'), | |
]; | |
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>(); | |
BookRoutePath get currentConfiguration => _selectedBook == null | |
? BookRoutePath.home() | |
: BookRoutePath.details(books.indexOf(_selectedBook)); | |
@override | |
Widget build(BuildContext context) { | |
return Navigator( | |
key: navigatorKey, | |
transitionDelegate: NoAnimationTransitionDelegate(), | |
pages: [ | |
MaterialPage( | |
key: ValueKey('BooksListPage'), | |
child: BooksListScreen( | |
books: books, | |
onTapped: _handleBookTapped, | |
), | |
), | |
if (_selectedBook != null) BookDetailsPage(book: _selectedBook) | |
], | |
onPopPage: (route, result) { | |
if (!route.didPop(result)) { | |
return false; | |
} | |
// Update the list of pages by setting _selectedBook to null | |
_selectedBook = null; | |
notifyListeners(); | |
return true; | |
}, | |
); | |
} | |
@override | |
Future<void> setNewRoutePath(BookRoutePath path) async { | |
if (path.isDetailsPage) { | |
_selectedBook = books[path.id]; | |
} | |
} | |
void _handleBookTapped(Book book) { | |
_selectedBook = book; | |
notifyListeners(); | |
} | |
} | |
class BookDetailsPage extends Page { | |
final Book book; | |
BookDetailsPage({ | |
this.book, | |
}) : super(key: ValueKey(book)); | |
Route createRoute(BuildContext context) { | |
return MaterialPageRoute( | |
settings: this, | |
builder: (BuildContext context) { | |
return BookDetailsScreen(book: book); | |
}, | |
); | |
} | |
} | |
class BookRoutePath { | |
final int id; | |
BookRoutePath.home() : id = null; | |
BookRoutePath.details(this.id); | |
bool get isHomePage => id == null; | |
bool get isDetailsPage => id != null; | |
} | |
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( | |
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 { | |
final Book book; | |
BookDetailsScreen({ | |
@required this.book, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(), | |
body: Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
if (book != null) ...[ | |
Text(book.title, style: Theme.of(context).textTheme.headline6), | |
Text(book.author, style: Theme.of(context).textTheme.subtitle1), | |
], | |
], | |
), | |
), | |
); | |
} | |
} | |
class NoAnimationTransitionDelegate extends TransitionDelegate<void> { | |
@override | |
Iterable<RouteTransitionRecord> resolve({ | |
List<RouteTransitionRecord> newPageRouteHistory, | |
Map<RouteTransitionRecord, RouteTransitionRecord> | |
locationToExitingPageRoute, | |
Map<RouteTransitionRecord, List<RouteTransitionRecord>> | |
pageRouteToPagelessRoutes, | |
}) { | |
final results = <RouteTransitionRecord>[]; | |
for (final pageRoute in newPageRouteHistory) { | |
if (pageRoute.isWaitingForEnteringDecision) { | |
pageRoute.markForAdd(); | |
} | |
results.add(pageRoute); | |
} | |
for (final exitingPageRoute in locationToExitingPageRoute.values) { | |
if (exitingPageRoute.isWaitingForExitingDecision) { | |
exitingPageRoute.markForRemove(); | |
final pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute]; | |
if (pagelessRoutes != null) { | |
for (final pagelessRoute in pagelessRoutes) { | |
pagelessRoute.markForRemove(); | |
} | |
} | |
} | |
results.add(exitingPageRoute); | |
} | |
return results; | |
} | |
} |
It's not easy to understand but declarative approach is much better.
Yes I agree the API can be simplified quite a bit, but once I got used to the API, I found that a lot of edge cases became easier to deal with. Our new routing setup is more robust and easier to customize than the one before.
Hopefully someone writes an easier to use API layer on top of it all
Is there a way to get a result from details page after pop back?
I'm curious how a login screen would fit in here. I want to have a login screen with one layout and after login I want to have several pages after login. Should I present the login screen as a traditional widget, use the traditional Navigator.push() to the "portal" and then within the portal I use the new router or should I set it up so that even the login screen is included in the new router? There is an example of nested router in the medium article but I do not think that its the way to go since they aren't nested not sure if I should use that for a single page (Login) and the rested in the sub router.
I would really like more examples of this router. Does anyone have public github repos of this?
I'm curious how a login screen would fit in here. I want to have a login screen with one layout and after login I want to have several pages after login. Should I present the login screen as a traditional widget, use the traditional Navigator.push() to the "portal" and then within the portal I use the new router or should I set it up so that even the login screen is included in the new router? There is an example of nested router in the medium article but I do not think that its the way to go since they aren't nested not sure if I should use that for a single page (Login) and the rested in the sub router.
I would really like more examples of this router. Does anyone have public github repos of this?
@mikeesouth, maybe this is the example you are looking for: https://github.com/dleurs/Flutter-Navigator-2.0-demo-with-Authentication-mecanism
@victormhg, it seems the issue with that is that it brings in bloc?
I am interested in the same question as @mikeesouth, but do want to keep it simple -- not load on additional frameworks.
Is there a way to get a result from details page after pop back?
I have same question!
2.0? no no no, just turn on 3.0.
I'm curious how a login screen would fit in here. I want to have a login screen with one layout and after login I want to have several pages after login. Should I present the login screen as a traditional widget, use the traditional Navigator.push() to the "portal" and then within the portal I use the new router or should I set it up so that even the login screen is included in the new router? There is an example of nested router in the medium article but I do not think that its the way to go since they aren't nested not sure if I should use that for a single page (Login) and the rested in the sub router.
I would really like more examples of this router. Does anyone have public github repos of this?
I see you comment was 8 months ago, can't find any decent example with a more complex navigation, would you have some sample to share?
Using this main.dart in a fresh flutter 2.2.3 project to kick the tyres and it doesn't run which isn't filling me with confidence.
Yeah you have to make it compliant with null safety:
import 'package:flutter/material.dart';
void main() {
runApp(const BooksApp());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksApp extends StatefulWidget {
const BooksApp({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _BooksAppState();
}
class _BooksAppState extends State<BooksApp> {
final BookRouterDelegate _routerDelegate = BookRouterDelegate();
final BookRouteInformationParser _routeInformationParser =
BookRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
}
}
class BookRouteInformationParser extends RouteInformationParser<BookRoutePath> {
@override
Future<BookRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location!);
if (uri.pathSegments.length >= 2) {
var remaining = uri.pathSegments[1];
return BookRoutePath.details(int.tryParse(remaining));
} else {
return BookRoutePath.home();
}
}
@override
RouteInformation? restoreRouteInformation(BookRoutePath configuration) {
if (configuration.isHomePage) {
return const RouteInformation(location: '/');
}
if (configuration.isDetailsPage) {
return RouteInformation(location: '/book/${configuration.id}');
}
return null;
}
}
class BookRouterDelegate extends RouterDelegate<BookRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<BookRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey;
Book? _selectedBook;
List<Book> books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
@override
BookRoutePath get currentConfiguration => _selectedBook == null
? BookRoutePath.home()
: BookRoutePath.details(books.indexOf(_selectedBook!));
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: const ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
if (_selectedBook != null) BookDetailsPage(book: _selectedBook)
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
_selectedBook = null;
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath configuration) async {
if (configuration.isDetailsPage) {
_selectedBook = books[configuration.id!];
}
}
void _handleBookTapped(Book book) {
_selectedBook = book;
notifyListeners();
}
}
class BookDetailsPage extends Page {
final Book? book;
BookDetailsPage({
this.book,
}) : super(key: ValueKey(book));
@override
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this,
builder: (BuildContext context) {
return BookDetailsScreen(book: book);
},
);
}
}
class BookRoutePath {
final int? id;
BookRoutePath.home() : id = null;
BookRoutePath.details(this.id);
bool get isHomePage => id == null;
bool get isDetailsPage => id != null;
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book> onTapped;
const BooksListScreen({Key? key,
required this.books,
required this.onTapped,
}) : super(key: key);
@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 {
final Book? book;
const BookDetailsScreen({Key? key,
required this.book,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (book != null) ...[
Text(book!.title, style: Theme.of(context).textTheme.headline6),
Text(book!.author, style: Theme.of(context).textTheme.subtitle1),
],
],
),
),
);
}
}
Hello, even though this thread is quite old, I hope that someone will help me with some questions / issues.
I am very happy that example is simple, however I see some issues with it mostly related to WEB implementation. I am trying to polish it to the level it behaves as consistent with native browser navigation as possible.
Example's main issue is with browser history: every time user presses back arrow it returns it to home screen, but instead of moving back in history, it just pushes new entry, so history is constantly growing (I belive that "back arrow" button executes Navigator.of(context).pop()
under the hood). Is that possible to make navigator's pop behavior the same as of browser's back button? I have solution for that:
html.window.history.go(-1);
but I am not sure it is the best one.
Second question is about page title (what we can see in tab and what we can see in browser history, like Application: Home for home or Application: Book Title for book details page).
I tried using Title widget, but I am not sure where I should put it in our example to make it work.
thanks a lot and looking forward some response from you.
an example is so complex