DEV Community

loading...
Cover image for Navigator 2.0 #Flutter

Navigator 2.0 #Flutter

Prakash S
A software developer interested in Flutter, Node & Dotnet
・4 min read

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

  1. Screen 1 -> Screen 2 (Push with parameter)
  2. Screen 2 -> Screen 1 (Pop)
  3. Screen 1 -> Screen 3 (Push with parameter)
  4. 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;
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
}


Enter fullscreen mode Exit fullscreen mode

And the final output
Alt Text of image

For full sample see GitHub

Happy Fluttering 😇😇

Discussion (0)