-
-
Save johnpryan/430c1d3ad771c43bf249c07fa3aeef14 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); | |
// Handle '/' | |
if (uri.pathSegments.length == 0) { | |
return BookRoutePath.home(); | |
} | |
// Handle '/book/:id' | |
if (uri.pathSegments.length == 2) { | |
if (uri.pathSegments[0] != 'book') return BookRoutePath.unknown(); | |
var remaining = uri.pathSegments[1]; | |
var id = int.tryParse(remaining); | |
if (id == null) return BookRoutePath.unknown(); | |
return BookRoutePath.details(id); | |
} | |
// Handle unknown routes | |
return BookRoutePath.unknown(); | |
} | |
@override | |
RouteInformation restoreRouteInformation(BookRoutePath path) { | |
if (path.isUnknown) { | |
return RouteInformation(location: '/404'); | |
} | |
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; | |
bool show404 = false; | |
List<Book> books = [ | |
Book('Left Hand of Darkness', 'Ursula K. Le Guin'), | |
Book('Too Like the Lightning', 'Ada Palmer'), | |
Book('Kindred', 'Octavia E. Butler'), | |
]; | |
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>(); | |
BookRoutePath get currentConfiguration { | |
if (show404) { | |
return BookRoutePath.unknown(); | |
} | |
return _selectedBook == null | |
? BookRoutePath.home() | |
: BookRoutePath.details(books.indexOf(_selectedBook)); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Navigator( | |
key: navigatorKey, | |
pages: [ | |
MaterialPage( | |
key: ValueKey('BooksListPage'), | |
child: BooksListScreen( | |
books: books, | |
onTapped: _handleBookTapped, | |
), | |
), | |
if (show404) | |
MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen()) | |
else 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; | |
show404 = false; | |
notifyListeners(); | |
return true; | |
}, | |
); | |
} | |
@override | |
Future<void> setNewRoutePath(BookRoutePath path) async { | |
if (path.isUnknown) { | |
_selectedBook = null; | |
show404 = true; | |
return; | |
} | |
if (path.isDetailsPage) { | |
if (path.id < 0 || path.id > books.length - 1) { | |
show404 = true; | |
return; | |
} | |
_selectedBook = books[path.id]; | |
} else { | |
_selectedBook = null; | |
} | |
show404 = false; | |
} | |
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; | |
final bool isUnknown; | |
BookRoutePath.home() | |
: id = null, | |
isUnknown = false; | |
BookRoutePath.details(this.id) : isUnknown = false; | |
BookRoutePath.unknown() | |
: id = null, | |
isUnknown = true; | |
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 UnknownScreen extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(), | |
body: Center( | |
child: Text('404!'), | |
), | |
); | |
} | |
} |
@johnpryan the example is not working, I got multiple errors, see screenshot attached.
@JamesVanWaza it looks like you need to migrate the code for null-safety
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!);
// Handle '/'
if (uri.pathSegments.length == 0) {
return BookRoutePath.home();
}
// Handle '/book/:id'
if (uri.pathSegments.length == 2) {
if (uri.pathSegments[0] != 'book') return BookRoutePath.unknown();
var remaining = uri.pathSegments[1];
var id = int.tryParse(remaining);
if (id == null) return BookRoutePath.unknown();
return BookRoutePath.details(id);
}
// Handle unknown routes
return BookRoutePath.unknown();
}
@override
RouteInformation? restoreRouteInformation(BookRoutePath path) {
if (path.isUnknown) {
return RouteInformation(location: '/404');
}
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;
bool show404 = false;
List<Book> books = [
Book('Left Hand of Darkness', 'Ursula K. Le Guin'),
Book('Too Like the Lightning', 'Ada Palmer'),
Book('Kindred', 'Octavia E. Butler'),
];
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
BookRoutePath get currentConfiguration {
if (show404) {
return BookRoutePath.unknown();
}
return _selectedBook == null
? BookRoutePath.home()
: BookRoutePath.details(books.indexOf(_selectedBook!));
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
if (show404)
MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
else 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;
show404 = false;
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(BookRoutePath path) async {
if (path.isUnknown) {
_selectedBook = null;
show404 = true;
return;
}
if (path.isDetailsPage) {
if (path.id! < 0 || path.id! > books.length - 1) {
show404 = true;
return;
}
_selectedBook = books[path.id!];
} else {
_selectedBook = null;
}
show404 = false;
}
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;
final bool isUnknown;
BookRoutePath.home()
: id = null,
isUnknown = false;
BookRoutePath.details(this.id) : isUnknown = false;
BookRoutePath.unknown()
: id = null,
isUnknown = true;
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 UnknownScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text('404!'),
),
);
}
}
To make sense of the Flutter Navigator 2.x and working in part off of this example, I created https://github.com/vgribok/flutter_nav2_oop example where navigator logic is abstracted away from the app logic for cleaner and consistent programming model.
I was able to get some routes working in my application using this. My URL doesn't change when I navigate to a page, but updating the URL myself will navigate to the right page. Does restoreRouteInformation
do nothing? I threw an UnimplementedError
in there just to check if it runs; nothing. What is it supposed to do? Why do I need it?
Hello, I have implemented the example along with a SearchDelegate, when I touch the search button of the searchDelegate, the application does not work, the android studio throws an error on the console:
The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget. When the exception was thrown, this was the stack:
#0 Navigator.of.<anonymous closure> (package:flutter/src/widgets/navigator.dart:2741:9)
#1 Navigator.of (package:flutter/src/widgets/navigator.dart:2748:6)
#2 showSearch (package:flutter/src/material/search.dart:70:20)
#3 ProductRouterDelegate.build.<anonymous closure> (package:flutter_app/main.dart:145:23)
#4 _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:989:21)
#5 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:193:24)
#6 TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:608:11)
I share you my code, I don´t have idea why this happens, any help is welcome, in advance I thank you for any advice.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_app/screen/cart.dart';
import 'package:flutter_app/screen/menu.dart';
import 'package:flutter_app/screen/product_detail.dart';
import 'package:flutter_app/widgets/product_list.dart';
import 'ext/ExtBottomNavigationBar.dart';
import 'model/Product.dart';
import 'widgets/banner.dart';
import 'widgets/product_offer.dart';
import 'package:http/http.dart' as http;
void main() => runApp(MainApp());
class MainApp extends StatefulWidget {
@override
_MainAppState createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
final ProductRouterDelegate _routerDelegate = ProductRouterDelegate();
BookRouteInformationParser _routeInformationParser =
BookRouteInformationParser();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
Container(
alignment: Alignment.topCenter,
color: Colors.red,
child: MainBanner(),
),
Column(
children: <Widget>[
Container(
alignment: Alignment.topLeft,
child: Text('Ofertas del día'),
),
Container(
alignment: Alignment.topCenter,
color: Colors.blue,
child: ProductOffer(),
),
],
),
ProductOffer(),
ProductOffer(),
],
),
);
}
}
class ProductRoutePath {
final int id;
final bool isUnknown;
ProductRoutePath.home()
: id = null,
isUnknown = false;
ProductRoutePath.details(this.id) : isUnknown = false;
ProductRoutePath.unknown()
: id = null,
isUnknown = true;
bool get isHomePage => id == null;
bool get isDetailsPage => id != null;
}
class ProductRouterDelegate extends RouterDelegate<ProductRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<ProductRoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
Product _selectedProduct;
bool show404 = false;
int _selectedIndex = 0;
List<Widget> _screenList = [HomePage(), CartScreen(), MenuScreen()];
ProductRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
List<Product> articles;
ProductRoutePath get currentConfiguration {
if (show404) {
return ProductRoutePath.unknown();
}
return _selectedProduct == null
? ProductRoutePath.home()
: ProductRoutePath.details(articles.indexOf(_selectedProduct));
}
void fetchProducts(String qSearch) async {
final response = await http.get(
'https://somedomain/articles_json.php?word=' + qSearch);
if (response.statusCode == 200) {
final parsed = jsonDecode(response.body).cast<Map<String, dynamic>>();
this.articles =
parsed.map<Product>((json) => Product.fromJson(json)).toList();
} else {
throw Exception('Failed to load articles');
}
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('HomePage'),
child: Scaffold(
appBar: AppBar(
title: Text("Home"),
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
showSearch(
context: context,
delegate:
DataSearch(onItemTapped: _handleBookTapped));
}),
],
),
body: SafeArea(
child: _screenList[_selectedIndex],
),
bottomNavigationBar:
extBottomNavigationBar(context, _selectedIndex, _onItemTapped),
),
),
if (show404)
MaterialPage(key: ValueKey('UnknownPage'), child: UnknownScreen())
else if (_selectedProduct != null)
ProductDetail(product: _selectedProduct)
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
_selectedProduct = null;
show404 = false;
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(ProductRoutePath path) async {
if (path.isUnknown) {
_selectedProduct = null;
show404 = true;
return;
}
if (path.isDetailsPage) {
if (path.id < 0 || path.id > articles.length - 1) {
show404 = true;
return;
}
_selectedProduct = articles[path.id];
} else {
_selectedProduct = null;
}
show404 = false;
}
void _handleBookTapped(Product product) {
_selectedProduct = product;
notifyListeners();
}
void _onItemTapped(int index) {
_selectedIndex = index;
notifyListeners();
}
}
class UnknownScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text('404!'),
),
);
}
}
class ProductRouteInformationParser
extends RouteInformationParser<ProductRoutePath> {
@override
Future<ProductRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
// Handle '/'
if (uri.pathSegments.length == 0) {
return ProductRoutePath.home();
}
// Handle '/article/:id'
if (uri.pathSegments.length == 2) {
if (uri.pathSegments[0] != 'article') return ProductRoutePath.unknown();
var remaining = uri.pathSegments[1];
var id = int.tryParse(remaining);
if (id == null) return ProductRoutePath.unknown();
return ProductRoutePath.details(id);
}
// Handle unknown routes
return ProductRoutePath.unknown();
}
@override
RouteInformation restoreRouteInformation(ProductRoutePath path) {
if (path.isUnknown) {
return RouteInformation(location: '/404');
}
if (path.isHomePage) {
return RouteInformation(location: '/');
}
if (path.isDetailsPage) {
return RouteInformation(location: '/article/${path.id}');
}
return null;
}
}
class BookRouteInformationParser
extends RouteInformationParser<ProductRoutePath> {
@override
Future<ProductRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
// Handle '/'
if (uri.pathSegments.length == 0) {
return ProductRoutePath.home();
}
// Handle '/article/:id'
if (uri.pathSegments.length == 2) {
if (uri.pathSegments[0] != 'book') return ProductRoutePath.unknown();
var remaining = uri.pathSegments[1];
var id = int.tryParse(remaining);
if (id == null) return ProductRoutePath.unknown();
return ProductRoutePath.details(id);
}
// Handle unknown routes
return ProductRoutePath.unknown();
}
@override
RouteInformation restoreRouteInformation(ProductRoutePath path) {
if (path.isUnknown) {
return RouteInformation(location: '/404');
}
if (path.isHomePage) {
return RouteInformation(location: '/');
}
if (path.isDetailsPage) {
return RouteInformation(location: '/article/${path.id}');
}
return null;
}
}
class DataSearch extends SearchDelegate<String> {
ValueChanged<Product> onItemTapped;
DataSearch({@required this.onItemTapped});
@override
List<Widget> buildActions(BuildContext context) {
// TODO: implement buildActions
return [
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
query = "";
})
];
}
@override
Widget buildLeading(BuildContext context) {
// TODO: implement buildLeading
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
close(context, null);
});
}
@override
Widget buildResults(BuildContext context) {
if (query.trim().length == 0) {
return Text("");
}
return FutureBuilder<List<Product>>(
future: fetchArticle(query),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ProductListScreen(
items: snapshot.data, onTapped: onItemTapped);
} else if (snapshot.hasError) {
return Center(
child: Text("${snapshot.error}"),
);
}
return Center(
child: CircularProgressIndicator(),
);
},
);
}
@override
Widget buildSuggestions(BuildContext context) {
if (query.trim().length > 0) {
return FutureBuilder<List<Product>>(
future: fetchArticle(query),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ProductListSuggestions(
articles: snapshot.data,
onPressed: _onPressed,
);
} else if (snapshot.hasError) {
return Center(
child: Text("${snapshot.error}"),
);
}
return Center(
child: CircularProgressIndicator(),
);
},
);
} else {
query = '';
return Text("");
}
}
void _onPressed(String dato) {
query = dato;
}
}
Future<List<Product>> fetchArticle(String qSearch) async {
final response = await http.get(
'https://somedomain/sidic/articles_json.php?word=' + qSearch);
if (response.statusCode == 200) {
return parseArticles(response.body);
} else {
throw Exception('Failed to load album');
}
}
List<Product> parseArticles(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Product>((json) => Product.fromJson(json)).toList();
}
class ProductListSuggestions extends StatelessWidget {
final List<Product> articles;
final onPressed;
ProductListSuggestions({Key key, this.articles, this.onPressed})
: super(key: key);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) {
return Row(
children: [
Expanded(
child: TextButton(
child: Text(articles[index].name),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProductDetailsScreen(product: articles[index]),
),
);
},
),
),
Expanded(
child: IconButton(
icon: Icon(Icons.add),
onPressed: () {
this.onPressed(articles[index].name);
},
),
)
],
);
},
);
}
}
I asked myself, where is the getter currentConfiguration
used?
Cannot understand why you declare and implement currentConfiguration
, but not reference to it. Until I read code of RouterDelegate
class.
Should be changed from:
BookRoutePath get currentConfiguration {
if (show404) {
return BookRoutePath.unknown();
}
return _selectedBook == null
? BookRoutePath.home()
: BookRoutePath.details(books.indexOf(_selectedBook));
}
to:
@override
BookRoutePath get currentConfiguration {
if (show404) {
return BookRoutePath.unknown();
}
return _selectedBook == null
? BookRoutePath.home()
: BookRoutePath.details(books.indexOf(_selectedBook));
}
I have continued to refine this sample into a usable starter project with tab-based navigation: https://github.com/vgribok/flutter_nav2_oop. I added Riverpod state management and state restoration, among quite a few other features. The example directory shows the sample app with nearly zero boilerplate.
Why keeping two form of the state, in BookRouterDelegate's attributes and in BookRoutePath ?
It would be simpler just to create in BookRouterDelegate a attribute called "BookRoutePath currentState = BookRoutePath.home();"
exemple here : https://github.com/dleurs/Flutter-Navigator-2.0-demo-with-Authentication-mecanism