In-depth Understanding of Navigator 2.0 in Flutter

Flutter’s Navigator 2.0 is a significant update to the navigation system, offering more control, flexibility, and predictability over routing and navigation in Flutter applications. Unlike the original Navigator, Navigator 2.0 exposes the underlying navigation stack, allowing developers to manage it programmatically. This provides more fine-grained control, making complex navigation scenarios, such as deep linking, easier to handle.

What is Navigator 2.0?

Navigator 2.0 introduces an imperative navigation API, replacing the declarative approach of Navigator 1.0. This means instead of relying on push and pop methods, developers can now directly manipulate the navigation stack. Key components of Navigator 2.0 include:

  • RouterDelegate: Manages the app’s navigation state and builds the navigator.
  • RouteInformationParser: Converts a RouteInformation object (usually from the URL) into a user-defined data type that the RouterDelegate can understand.
  • RouteInformationProvider: Provides route information to the router. The default implementation is PlatformRouteInformationProvider, which gets the initial route from the platform.
  • BackButtonDispatcher: Dispatches back button presses to the active route.

Why Use Navigator 2.0?

Navigator 2.0 is particularly beneficial for apps requiring:

  • Deep Linking: Handling navigation from external links.
  • Complex Navigation: Apps with intricate navigation patterns that are hard to manage with the original Navigator.
  • Web Support: Building Flutter apps that run in a browser, where URL management is critical.
  • Custom Navigation Logic: Implementing advanced routing strategies.

How to Implement Navigator 2.0

Implementing Navigator 2.0 involves several steps, from setting up the RouterDelegate to handling route information.

Step 1: Create a Custom Route Information Parser

The RouteInformationParser converts the RouteInformation (e.g., URL from a web browser) into a data type understood by your RouterDelegate.


import 'package:flutter/widgets.dart';

class MyRouteInformationParser extends RouteInformationParser<RouteSettings> {
  @override
  Future<RouteSettings> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.uri.toString());

    // Handle initial/unknown routes
    if (uri.pathSegments.isEmpty) {
      return const RouteSettings(name: '/home');
    }

    // Parse the route and return RouteSettings
    return RouteSettings(name: '/${uri.pathSegments.first}');
  }

  @override
  RouteInformation? restoreRouteInformation(RouteSettings configuration) {
    if (configuration.name == null) {
      return const RouteInformation(uri: Uri.parse('/home'));
    }
    return RouteInformation(uri: Uri.parse(configuration.name!));
  }
}

In this example:

  • parseRouteInformation takes a RouteInformation object and parses it into a RouteSettings.
  • restoreRouteInformation converts the RouteSettings back into a RouteInformation. This is used when the app needs to update the browser URL.

Step 2: Implement the Router Delegate

The RouterDelegate is responsible for building the navigator and managing the app’s navigation stack. It listens to changes and rebuilds the UI accordingly.


import 'package:flutter/material.dart';

class MyRouterDelegate extends RouterDelegate<RouteSettings>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteSettings> {
  RouteSettings _currentConfiguration = const RouteSettings(name: '/home');

  @override
  GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  RouteSettings get currentConfiguration => _currentConfiguration;

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        if (_currentConfiguration.name == '/home')
          const MaterialPage(
            key: ValueKey('HomePage'),
            child: HomePage(),
          )
        else if (_currentConfiguration.name == '/details')
          const MaterialPage(
            key: ValueKey('DetailsPage'),
            child: DetailsPage(),
          ),
      ],
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }
        _currentConfiguration = const RouteSettings(name: '/home');
        notifyListeners();
        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(RouteSettings configuration) async {
    _currentConfiguration = configuration;
    notifyListeners();
  }

  void goToDetails() {
    _currentConfiguration = const RouteSettings(name: '/details');
    notifyListeners();
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to Details'),
          onPressed: () {
            MyRouterDelegate delegate =
                Router.of(context).routerDelegate as MyRouterDelegate;
            delegate.goToDetails();
          },
        ),
      ),
    );
  }
}

class DetailsPage extends StatelessWidget {
  const DetailsPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Details')),
      body: const Center(
        child: Text('Details Page'),
      ),
    );
  }
}

Key components of the RouterDelegate:

  • navigatorKey: A global key for the Navigator.
  • currentConfiguration: Holds the current route settings.
  • build: Constructs the Navigator based on the currentConfiguration. It uses a list of Page widgets to define the navigation stack.
  • setNewRoutePath: Updates the currentConfiguration based on the new route. This is called when the browser URL changes (e.g., deep linking).
  • onPopPage: Handles popping pages from the navigator stack.

Step 3: Configure the App Widget

Wrap your top-level widget with the Router widget, providing instances of your custom RouterDelegate and RouteInformationParser.


import 'package:flutter/material.dart';
import 'package:navigator_2_example/route_information_parser.dart';
import 'package:navigator_2_example/router_delegate.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  MyRouterDelegate _routerDelegate = MyRouterDelegate();
  MyRouteInformationParser _routeInformationParser = MyRouteInformationParser();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Navigator 2.0 Example',
      routerDelegate: _routerDelegate,
      routeInformationParser: _routeInformationParser,
      backButtonDispatcher: RootBackButtonDispatcher(),
    );
  }
}

Here, the MaterialApp.router is configured with:

  • routerDelegate: An instance of MyRouterDelegate.
  • routeInformationParser: An instance of MyRouteInformationParser.
  • backButtonDispatcher: Manages the back button presses.

Complete Example

Below is the complete runnable code that combines all the steps described above. You can directly use it in your Flutter project.


// main.dart
import 'package:flutter/material.dart';
import 'route_information_parser.dart';
import 'router_delegate.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final MyRouterDelegate _routerDelegate = MyRouterDelegate();
  final MyRouteInformationParser _routeInformationParser =
      MyRouteInformationParser();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Navigator 2.0 Example',
      routerDelegate: _routerDelegate,
      routeInformationParser: _routeInformationParser,
      backButtonDispatcher: RootBackButtonDispatcher(),
    );
  }
}

// route_information_parser.dart
import 'package:flutter/widgets.dart';

class MyRouteInformationParser extends RouteInformationParser<RouteSettings> {
  @override
  Future<RouteSettings> parseRouteInformation(
      RouteInformation routeInformation) async {
    final uri = Uri.parse(routeInformation.uri.toString());

    if (uri.pathSegments.isEmpty) {
      return const RouteSettings(name: '/home');
    }

    return RouteSettings(name: '/${uri.pathSegments.first}');
  }

  @override
  RouteInformation? restoreRouteInformation(RouteSettings configuration) {
    if (configuration.name == null) {
      return const RouteInformation(uri: Uri.parse('/home'));
    }
    return RouteInformation(uri: Uri.parse(configuration.name!));
  }
}

// router_delegate.dart
import 'package:flutter/material.dart';

class MyRouterDelegate extends RouterDelegate<RouteSettings>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteSettings> {
  RouteSettings _currentConfiguration = const RouteSettings(name: '/home');

  @override
  GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  @override
  RouteSettings get currentConfiguration => _currentConfiguration;

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        if (_currentConfiguration.name == '/home')
          const MaterialPage(
            key: ValueKey('HomePage'),
            child: HomePage(),
          )
        else if (_currentConfiguration.name == '/details')
          const MaterialPage(
            key: ValueKey('DetailsPage'),
            child: DetailsPage(),
          ),
      ],
      onPopPage: (route, result) {
        if (!route.didPop(result)) {
          return false;
        }
        _currentConfiguration = const RouteSettings(name: '/home');
        notifyListeners();
        return true;
      },
    );
  }

  @override
  Future<void> setNewRoutePath(RouteSettings configuration) async {
    _currentConfiguration = configuration;
    notifyListeners();
  }

  void goToDetails() {
    _currentConfiguration = const RouteSettings(name: '/details');
    notifyListeners();
  }
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to Details'),
          onPressed: () {
            MyRouterDelegate delegate =
                Router.of(context).routerDelegate as MyRouterDelegate;
            delegate.goToDetails();
          },
        ),
      ),
    );
  }
}

class DetailsPage extends StatelessWidget {
  const DetailsPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Details')),
      body: const Center(
        child: Text('Details Page'),
      ),
    );
  }
}

Conclusion

Navigator 2.0 provides Flutter developers with the tools needed for complex navigation scenarios, including deep linking and web support. While it requires a deeper understanding and more code compared to the original Navigator, the flexibility and control it offers make it invaluable for sophisticated applications. By implementing custom RouteInformationParser and RouterDelegate, you can manage navigation in a way that perfectly fits your application’s needs.