Navigation 2.0 - Routing On Flutter Web
Learn how to sync web URLs with Flutter pages and handle navigation on web using Flutter
In this tutorial we will learn how Navigation 2.0 works with Flutter Web, how we can build webpages and sync the URL with our Flutter Web projects.
Previously, with imperative navigation techniques, we were only able to push and pop routes in the navigation stack, but that did not handle the web URL and web histories. So, the Flutter team came up with this new declarative navigation technique that handles URLs and histories as an integral part of route management.
Let's look at the different techniques that we can use to manage Routes and how Navigation 2.0 makes it better.
Routing using OnGenerateRoute
Let’s start with Routing using onGenerateRoute first. If you try navigating to different pages using simple Navigator.push()
or other methods, the page will change but the URL won’t. Also, there will be no history saved for this.
We can do this using Generate Routes.
onGenerateRoutes
is the route generator callback used when the app is navigated to a named route. Using this, we will generate routes, navigate to different page and sync the changes with the URL of the browser.
We need two route files (for cleaner code), one for defining the route names and other for defining the onGenerateRoute
class.
The routes_name
file will have the routes with corresponding route names.
class RoutesName {
// ignore: non_constant_identifier_names
static const String FIRST_PAGE = '/first_page';
// ignore: non_constant_identifier_names
static const String SECOND_PAGE = '/second_page';
}
Here, we have two pages, FIRST_PAGE
(Example: localhost:3000/#/first_page) and SECOND_PAGE
(Example: localhost:3000/#/second_page).
Now, we will create a reusable onGenerateRoute
class in our second routes file.
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case RoutesName.FIRST_PAGE:
return _GeneratePageRoute(
widget: FirstPage(), routeName: settings.name);
case RoutesName.SECOND_PAGE:
return _GeneratePageRoute(
widget: SecondPage(), routeName: settings.name);
default:
return _GeneratePageRoute(
widget: FirstPage(), routeName: settings.name);
}
}
Using the route settings, we will get the Routes name and then navigate to the corresponding Page. Here, the _GeneratePageRoute
is the class extending PageRouteBuilder
. It is used to add navigation transition animations.
class _GeneratePageRoute extends PageRouteBuilder {
final Widget widget;
final String routeName;
_GeneratePageRoute({this.widget, this.routeName})
: super(
settings: RouteSettings(name: routeName),
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return widget;
},
transitionDuration: Duration(milliseconds: 500),
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return SlideTransition(
textDirection: TextDirection.rtl,
position: Tween<Offset>(
begin: Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: child,
);
});
}
Setting routes is almost completed. The Final step is to add the generateRoute
to our main.dart
file.
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
builder: (context, child) => HomePage(child: child),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: RoutesName.FIRST_PAGE,
);
}
}
Inside the MaterialApp
, we will add the routes to onGenerateRoute
property and set one initial route (This page will be loaded first and the corresponding route will be the initial route that will be shown on the URL too). Now, using builder property of material app, we will insert the widgets. HomePage
is a stateless widget that builds the inserted widget.
class HomePage extends StatelessWidget {
final Widget child;
const HomePage({Key key, this.child}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
);
}
}
We have two pages for our two routes ( /first-page and /second-page). To navigate from one page to another, we will use Navigator.pushNamed(context, RouteName);
First Page:
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 400,
child: Column(
children: [
Container(
child: Center(
child: Text("FIRST PAGE"),
),
),
TextButton(
onPressed: () {
Navigator.pushNamed(context, RoutesName.SECOND_PAGE);
},
child: Text("NAVIGATE")),
],
),
);
}
}
Second Page:
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 400,
child: Column(
children: [
Container(
child: Center(
child: Text("SECOND PAGE"),
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
// Navigator.pushNamed(context, RoutesName.SECOND_PAGE);
},
child: Text("NAVIGATE")),
],
),
);
}
}
Our routing part is now completed.
Now, let's look at another method of implementing Routing in Flutter Web using Navigation 2.0.
Routing Using Navigation 2.0
Navigation 2.0 follows a declarative approach. Using this approach, we will sync a web URL with our Flutter project.
Let’s see how we can do this.
We will use a constructor named MaterialApp.router()
. It creates a material app that uses the router instead of Navigator. It requires two things, routerDelegate
and routerInformationParser
.
The URL typed will pass through RouterInformationParser
and it will convert the URL into user defined data. Then, this will be passed to RouterDelegate
, which is in charge of the states of the router app. It will render according to the consumed data.
Let’s first create our generic data type in which route will be converted through RouteInformationParser
.
class HomeRoutePath {
final String pathName;
final bool isUnkown;
HomeRoutePath.home()
: pathName = null,
isUnkown = false;
HomeRoutePath.otherPage(this.pathName) : isUnkown = false;
HomeRoutePath.unKown()
: pathName = null,
isUnkown = true;
bool get isHomePage => pathName == null;
bool get isOtherPage => pathName != null;
}
In the above class, we have two variables, pathName
, which will be the URL param (after “/“) and boolean
, that will show the 'unknown page' in case of an invalid URL.
Also, we have created some named constructors.
home()
shows the initial screen when route is “/“. In this case, the unknown boolean will be false and the path will be empty.
otherPage()
is for the pages with pathName
. Eg. /xyz.
unknown()
is for unknown paths.
After this, we will create the RouteInformationParser
which will convert URL into our HomeRoutePath
.
class HomeRouteInformationParser extends RouteInformationParser<HomeRoutePath> {
@override
Future<HomeRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
if (uri.pathSegments.length == 0) {
return HomeRoutePath.home();
}
if (uri.pathSegments.length == 1) {
final pathName = uri.pathSegments.elementAt(0).toString();
if (pathName == null) return HomeRoutePath.unKown();
return HomeRoutePath.otherPage(pathName);
}
return HomeRoutePath.unKown();
}
@override
RouteInformation restoreRouteInformation(HomeRoutePath homeRoutePath) {
if (homeRoutePath.isUnkown) return RouteInformation(location: '/error');
if (homeRoutePath.isHomePage) return RouteInformation(location: '/');
if (homeRoutePath.isOtherPage)
return RouteInformation(location: '/${homeRoutePath.pathName}');
return null;
}
}
Our class will extend RouterInformationParser
with type HomeRoutePath
and then we will override the parseRouteInformation
method that is responsible for parsing URL path into HomeRoutePath
.
When we will have zero path segments (pathSegements after “/“ eg /abc/xyz has 2 path segments), we will convert to home()
type. Similarly, we will convert to other types as per the conditions.
We have another method to override, called the restoreRouteInformation
. This method will store the browsing history in the browser. Here, we have passed the path to the RouteInformation
as per the HomeRoutePath
state.
We have now completed informationParser
.
Let’s move to RouterDelegate
.
This class need to extend RouterDelegate
with type HomeRoutePath
. It has 5 override methods and we need not to handle all of them. For the addListener
and removeListener
method, we can use the ChangeNotifier
and add it to the function.
Also for popRoute
, we can use the mixin PopNavigatorRouterDelegateMixin
.
class HomeRouterDelegate extends RouterDelegate<HomeRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<HomeRoutePath> {
String pathName;
bool isError = false;
@override
GlobalKey<NavigatorState> get navigatorKey => GlobalKey<NavigatorState>();
@override
HomeRoutePath get currentConfiguration {
if (isError) return HomeRoutePath.unKown();
if (pathName == null) return HomeRoutePath.home();
return HomeRoutePath.otherPage(pathName);
}
void onTapped(String path) {
pathName = path;
print(pathName);
notifyListeners();
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey('HomePage'),
child: HomePage(
onTapped: (String path) {
pathName = path;
notifyListeners();
},
),
),
if (isError)
MaterialPage(key: ValueKey('UnknownPage'), child: UnkownPage())
else if (pathName != null)
MaterialPage(
key: ValueKey(pathName),
child: PageHandle(
pathName: pathName,
))
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
pathName = null;
isError = false;
notifyListeners();
return true;
});
}
@override
Future<void> setNewRoutePath(HomeRoutePath homeRoutePath) async {
if (homeRoutePath.isUnkown) {
pathName = null;
isError = true;
return;
}
if (homeRoutePath.isOtherPage) {
if (homeRoutePath.pathName != null) {
pathName = homeRoutePath.pathName;
isError = false;
return;
} else {
isError = true;
return;
}
} else {
pathName = null;
}
}
}
Here, we have defined two states that we will use for navigating to pages. One is pathName
and other is for error pages isError
.
Let’s start with override methods one by one:
currentConfiguration
: is called by the [Router] when it detects a route information may have changed as a result of a rebuild. So, according to the conditions, we are calling the HomePageRoute
constructors.
setNewRoutePath
: This method handles the routing when user enters URL in the browser and presses enter.
build
: Returns the Navigator with our HomePage
by default and changing the state through HomePage
and if pathName
is not null, we can map the name with pages and show different pages as per different routes (here I’m just showing one page with route names).
The final Step is to add our routerDelegate
and routerInformationParser
to MaterialApp.router()
inside main.dart
.
void main() {
runApp(App());
}
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: "Flutter Navigaton 2.0",
routerDelegate: HomeRouterDelegate(),
routeInformationParser: HomeRouteInformationParser(),
);
}
}
Conclusion
The approach involving Navigation 2.0 has too much boilerplate code to add but it handles the cases that are missed in the onGenerateRoute
approach. You may notice that the forward arrow in the browser might not work properly with first approach. Still, the first approach is somewhat easier to implement and manage compared to the second.
Resources
flutter.dev/docs/cookbook/animation/page-ro..
api.flutter.dev/flutter/widgets/WidgetsApp/..