# Navigation 2.0 - Routing On Flutter Web

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: ﻿http://localhost:3000/#/first_page) and `SECOND_PAGE` (Example: http://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.


![routing.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1612898993511/2EZLddAEQ.png)

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

https://flutter.dev/docs/cookbook/animation/page-route-animation

https://api.flutter.dev/flutter/widgets/WidgetsApp/onGenerateRoute.html

https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html

https://medium.com/flutter/learning-flutters-new-navigation-and-routing-system-7c9068155ade

