I am writing this post to make sure flutter devs not to struggle to do the navigation in flutter with Navigator 2.0
Why because, i have struggled a lot to get to know this concept. To be honest, I am not sure did i got all necessary knowledge on this.
First of all, why we need to struggle to know Navigator 2.0. Legacy Navigator itself good isnt it. Yes, it is good for Mobile apps, whereas for website functionality like histories, url update are quite tedious in the old navigator. That is the only reason we are going to Navigator 2.0. The helllllll π‘π‘π‘
But I bet after going through this post, you might feel it would be enough to do the navigation with ease for your Flutter web/mobile apps.
Let's stop reading and dive into the code
We are going to take a real time scenario with 3 screens
- Screen 1 -> Screen 2 (Push with parameter)
- Screen 2 -> Screen 1 (Pop)
- Screen 1 -> Screen 3 (Push with parameter)
- Screen 2 -> Screen 3 -> Screen 1 (PopUntil to remove some page stack)
Key things we need to have for Navigator 2.0
RouteInformationParser
RouterDelegate
User Object/Configuration (in this article i will consider to say as user object)
User object π
This is nothing but our own data structure to proceed navigation to serve our purpose.
class UserObject {
final Type? pageType;
final String? param;
final bool isUnknown;
UserObject.screen1()
: pageType = Screen1,
param = null,
isUnknown = false;
UserObject.screen2(String parameter)
: pageType = Screen2,
param = parameter,
isUnknown = false;
UserObject.screen3(String parameter)
: pageType = Screen3,
param = parameter,
isUnknown = false;
UserObject.unKnown()
: pageType = null,
param = null,
isUnknown = true;
}
RouteInformationParser π³οΈ
This deals majorly two things
- Parse the route informations like url location, path parameters & query parameters and returns the desired user object.
- Restore the route information from user object.
class AppRouteInformationParser extends RouteInformationParser<UserObject> {
@override
Future<UserObject> parseRouteInformation(
RouteInformation routeInformation) async {
if (routeInformation.location == null) {
UserObject.unKnown();
}
final uri = Uri.parse(routeInformation.location!);
if (uri.pathSegments.length == 2) {
final remaining = uri.pathSegments.last;
final first = uri.pathSegments.first;
if ('$first' == 'screen2') {
return UserObject.screen2(remaining);
}
if ('/$first' == 'screen3') {
return UserObject.screen3(remaining);
}
return UserObject.unKnown();
}
return UserObject.screen1();
}
@override
RouteInformation? restoreRouteInformation(UserObject configuration) {
RouteInformation error = RouteInformation(location: '/404');
if (configuration.pageType == null) {
return error;
}
switch (configuration.pageType!) {
case Screen1:
return RouteInformation(location: '/');
case Screen2:
return RouteInformation(location: '/screen2/${configuration.param}');
case Screen3:
return RouteInformation(location: '/screen3/${configuration.param}');
default:
return error;
}
}
}
RouterDelegate πͺ
I would say this is the heart for the navigation, where all our navigation logic would reside here.
So what basically we may need for navigation
- Routes/Route history
- Push/Go Forward
- Pop/Go Backward
We need to intimate our operations to the framework, to do this we have certain overrides
- setNewRoutePath
- currentConfiguration
Will deep dive RouterDelegate for better understanding
Very first we need to create our router delegate and extend from framework RouterDelegate and add a mixin with ChangeNotifier & PopNavigatorRouterDelegateMixin
class AppRouterDelegate extends RouterDelegate<UserObject>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<UserObject>
with respect to this, we need to provide 3 information to the framework
- setNewRoutePath method
- navigatorKey
- build method
Along with this we are going to maintain the page history via userObjects property
the idea is simple on push operation we are adding the page to userObjects and providing the same to Navigator widget, similarly we are updating/removing the entry in userObjects on pop operation.
extension RouteExtension on BuildContext {
Future<void> navigateTo(UserObject path) async {
(Router.of(this).routerDelegate as AppRouterDelegate).push(path);
}
Future<bool> pop() async {
return await (Router.of(this).routerDelegate as AppRouterDelegate)
.popRoute();
}
Future<bool> popUntil(UserObject path) async {
AppRouterDelegate deletgate =
(Router.of(this).routerDelegate as AppRouterDelegate);
while (deletgate.userObjects.last.pageType != path.pageType!) {
await this.pop();
}
return true;
}
}
class AppRouterDelegate extends RouterDelegate<UserObject>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<UserObject> {
GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;
List<UserObject> _userObjects = [];
List<UserObject> get userObjects => _userObjects;
bool _canPop = true;
GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
bool get canPop {
if (_canPop == false) return false;
return _userObjects.isNotEmpty;
}
set canPop(bool canPop) => _canPop = canPop;
UserObject? get currentConfiguration =>
userObjects.isNotEmpty ? userObjects.last : null;
@override
Widget build(BuildContext context) {
return Navigator(
key: _navigatorKey,
pages: [
if (userObjects.isEmpty) PageBuilder(UserObject.screen1()).page,
for (UserObject path in userObjects) PageBuilder(path).page,
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
if (canPop) {
pop();
}
return true;
});
}
@override
Future<void> setNewRoutePath(UserObject path) async {
if (_canPop == false) return SynchronousFuture(null);
if (path == currentConfiguration) return SynchronousFuture(null);
_userObjects = _setNewRouteHistory(_userObjects, path);
notifyListeners();
return SynchronousFuture(null);
}
@override
Future<bool> popRoute() {
print('Pop Route');
return super.popRoute();
}
List<UserObject> _setNewRouteHistory(
List<UserObject> routes, UserObject newRoute) {
List<UserObject> pathsHolder = [];
pathsHolder.addAll(routes);
// Check if new path exists in history.
for (UserObject path in routes) {
// If path exists, remove all paths on top.
if (path.pageType == newRoute.pageType) {
int index = routes.indexOf(path);
int count = routes.length;
for (var i = index; i < count - 1; i++) {
pathsHolder.removeLast();
}
return pathsHolder;
}
}
pathsHolder.add(newRoute);
return pathsHolder;
}
void push(UserObject path) {
_userObjects.add(path);
notifyListeners();
}
void pop() {
_userObjects.removeLast();
notifyListeners();
}
}
class PageBuilder {
final UserObject homeRoutePath;
PageBuilder(this.homeRoutePath);
Page getPage(UserObject path) {
switch (path.pageType) {
case Screen1:
return MaterialPage(child: Screen1());
case Screen2:
return MaterialPage(
key: ValueKey('screen2 ${path.param}'),
child: Screen2(
path.param.toString(),
));
case Screen3:
return MaterialPage(
key: ValueKey('screen3 ${path.param}'),
child: Screen3(path.param.toString()));
default:
return MaterialPage(
child: Scaffold(
body: Center(
child: Text('Unknown route'),
),
));
}
}
dynamic get page => getPage(homeRoutePath);
}
For full sample see GitHub
Happy Fluttering ππ
Top comments (0)