Flutter Navigator 2.0 for web dev: Url handling
This article is part of a series aiming at making you a master of Navigator 2.0 while showing you that it is not as hard as people think. The other articles of this series are:
- Flutter Navigator 2.0: 101 for mobile dev: Were to start if you have no idea what navigator 2.0 is.
- Flutter Navigator 2.0 for mobile dev: Nested navigators basics: How to nest the navigators, and the fix needed to avoid that they ruin your animations
- Flutter Navigator 2.0 for mobile dev: Transitions: How to implement Hero or Page route transitions with navigator 2.0
- Flutter Navigator 2.0 for mobile dev: Bloc state management integration: How to use navigator 2.0 and the bloc library
- Flutter web: URL handling made simple with simple_url_handler: How to use the simple_url_handler package to easily manage the url in flutter web.
- Flutter web: A complete example using simple_url_handler: A complete example, using everything seen above, to build the routing and url handling of a more complete app.
Url handling was one of the biggest promise of navigator 2.0. And by “url handling” I mean:
- Changing the state of your app updates the url
- Changing your url changes your app state
- The forward and backward button on a browser work
In short: bringing the expected web experience to our app.
What you already know is that this is now possible, what you don’t know is how simple this is.
Note that this article is NOT about routing. This is really important because flutter introduced us to routing and url handling at the same time and this made things messy. I personally believe that you should think about them as two different things.
However, you must understand what Router
and RouteDelegate
are, so if you don’t, check my article Flutter Navigator 2.0: 101 for mobile dev.
Now, let’s look at a very clear schema of what is happening:
Just kidding, here is the real deal:
The theory
When you enter a new url:
parseInformationRouter
is called, which should convert your url into an Navigation state (wee above what this is).- Then,
setNewRoutePath
is called, and should convert the navigation state into your app state.
When your app state change:
- Call
routerDelegate.notifyListeners()
to warn flutter that the url should be synchronized currentConfiguration
is called which should convert your app state into a navigation state.restoreRouteInformation
is called which converts your navigation state into a url.
What is the navigation state? It is a class which should hold all the information about the navigation. Why does this exist instead of just having the url as a navigation state? I don’t know, maybe because parsing the url separately (ie in RouteInformationParser
is safer). Don’t bother yourself with that, just accept it.
What is RouteInformationParser? If you already understand routing, the routerDelegate
should sound familiar, however the RouteInformationParser
should not. It is just a class which we will implement and override the methods parseRouteInformation
and restoreRouteInformation
.
Our example app
You can find the code at the end of this article
This is the basic counter app and the url is equal to 10*count
.
Has we saw, we will need to create a RouteInformationParser
and a RouterDelegate
. To use the, as when routing, we will need to pass them to a Router
widget. We also need a class to hold our navigation state.
Navigation State
Here we just need to keep track of the counter so our class is simple:
The RouteInformationParser
parseRouteInformation:
Called when the url is updated. Its job is to convert the url (contained in routeInformation.location) into the navigation state. Here the url is /counter
to we remove the first character and parse the rest. Also note that if the url is not valid, navigationState.value is null.
restoreRouteInformation:
This is the inverse function of parseRouteInformation
, it takes a NavigationState and convert it into a url. This is called after routerDelegate.currentConfiguration
which we will see next.
The RouterDelegate
The build
is not really interesting, it is like any RouterDelegate
. Just don’t forget that even if we don’t use routing, having a navigator is interesting because if you don’t have any in you app it can break things (such as overlays).
The interesting part are: setNewRoutePath
, currentConfiguration
and increase
:
setNewRoutePath
Called right after routeInformationParser.parseRouteInformation
. It’s job it to convert the navigation state into your app state. This is where all your logic should take place: verifying that the user can access what he/she is trying to access, redirecting if needed. In our case, we check if navigationState.value
is null (remember that this is what happens if the url is not valid). If it’s null, we don’t modify our state but call notifyListener
so that our url changes back to a correct one. If it is not null, we compute navigationState.value/10
and take the int part. Then we check if the url is in sync with the new counter, if not we call notifyListener
.
currentConfiguration
This is called when notifyListener
is called. It’s job is to change the app state into the navigation state. When this function ends, routeInformationParser.restoreRouteInformation
is called with this new navigation state.
The Router
We just need to put everything into a router and we are done! However note that you can’t use MaterialApp
and a Router
which handles the url at the same time for now. This is because MaterialApp
belongs to Navigator 1 and Router is navigator 2 so their is a conflict. To prevent this, use MaterialApp.router
which create a MaterialApp
and a Router
. Also remember to not use MaterialApp
down your widget tree since the same issue will occur. This forces us to have our router really up the widget tree but with most state management libraries this is not an issue.
That’s it
Go back to the theoretical schema that I show you, and you will see that you understand everything and that you can generalize this to any app!
Here is the entire code for those who just want to copy/paste 😉
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(UrlHandler());
}
class UrlHandler extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: UrlHandlerRouterDelegate(),
routeInformationParser: UrlHandlerInformationParser(),
);
}
}
class NavigationState {
final int value;
NavigationState(this.value);
}
final GlobalKey<NavigatorState> _urlHandlerRouterDelegateNavigatorKey =
GlobalKey<NavigatorState>();
class UrlHandlerRouterDelegate extends RouterDelegate<NavigationState>
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
int count = 0;
@override
Widget build(BuildContext context) {
return Navigator(
pages: [
MaterialPage(child: MyHomePage(count: count, increase: increase)),
],
onPopPage: (_, __) {
// We don't handle routing logic here, so we just return false
return false;
},
);
}
@override
GlobalKey<NavigatorState> get navigatorKey => _urlHandlerRouterDelegateNavigatorKey;
// Navigation state to app state
@override
Future<void> setNewRoutePath(NavigationState navigationState) {
// If a value which is not a number has been entered,
// navigationState.value is null so we just notifyListeners
// without changing the app state to change the value of the url
// to its previous value
if (navigationState.value == null) {
notifyListeners();
return null;
}
// Get the new count, which is navigationState.value//10
count = (navigationState.value / 10).floor();
// If the navigationState.value was not a multiple of 10
// the url is not equal to count*10, therefore the url isn't right
// In that case, we notifyListener in order to get the valid NavigationState
// from the new app state
if (count * 10 != navigationState.value) notifyListeners();
return null;
}
// App state to Navigation state, triggered by notifyListeners()
@override
NavigationState get currentConfiguration => NavigationState(count*10);
void increase() {
count++;
notifyListeners();
}
}
class UrlHandlerInformationParser extends RouteInformationParser<NavigationState> {
// Url to navigation state
@override
Future<NavigationState> parseRouteInformation(RouteInformation routeInformation) async {
return NavigationState(int.tryParse(routeInformation.location.substring(1)));
}
// Navigation state to url
@override
RouteInformation restoreRouteInformation(NavigationState navigationState) {
return RouteInformation(location: '/${navigationState.value}');
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.count, this.increase}) : super(key: key);
final int count;
final VoidCallback increase;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${widget.count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
widget.increase();
},
tooltip: 'Counter',
child: Icon(Icons.add),
),
);
}
}