DEV Community

Cover image for Flutter Navigation: Navigator vs Go Router
Amaury for Rootstrap

Posted on

Flutter Navigation: Navigator vs Go Router

In this post we are going to learn a little bit more about Flutter Navigation and go_router.

Check the original POST

How does flutter navigation work?

Flutter navigation provides a way for you to navigate from one widget to another in your app. This is achieved by using a navigation stack, which is essentially a collection of routes.

Navigator is a built-in Flutter widget that provides a simple and convenient way to manage the navigation stack and navigate between routes. It's straightforward to use and can be a good choice for basic navigation needs. On it a route represents a widget and its associated data. When you navigate to a new route, it is pushed onto the navigation stack, and when you navigate back, the current route is popped off the stack.

Navigator provides a number of navigation widgets, including Navigator, MaterialPageRoute, and CupertinoPageRoute, that make it easy to manage the navigation stack and provide common navigation patterns.

With Navigator, you can push and pop routes, as well as manage the navigation stack. MaterialPageRoute and CupertinoPageRoute are two common types of routes in Flutter that are used to transition between pages. They provide animation and visual effects appropriate for the material design or Cupertino design languages, respectively.

Overall, Flutter navigation is designed to be flexible, allowing you to create complex navigation patterns that meet the needs of your app.

But there are some limitations and potential problems that you should be aware of:

  • Limited routing features: Navigator provides basic routing functionality, but it doesn't offer advanced features like named routes, path parameters, query parameters, or wildcard matching, which can make it challenging to build more complex navigation patterns.
  • Code complexity: As your app grows in complexity, managing the navigation stack with Navigator can become more challenging, making your code more difficult to maintain and debug.
  • Lack of customizability: Navigator provides basic animation and visual effects for transitioning between routes, but it doesn't allow you to customize the routing transitions and animations.
  • Global state management: Navigator doesn't provide a built-in solution for managing the global states, which can make it challenging to share data between routes.
  • Inconsistent navigation patterns: Because Navigator is a low-level widget, it can be challenging to ensure that navigation patterns are consistent across your app. This can lead to a fragmented user experience.
  • These limitations can make it challenging to build complex, scalable, and consistent navigation patterns with Navigator. If you encounter these limitations, you may want to consider using a third-party routing library, such as go_router, to address them.
  • On the other hand, go_router is a third-party routing library maintained by the Flutter team, that provides advanced routing functionality that is not available in Navigator. For example, go_router provides features such as named routes, path parameters, query parameters, and wildcard matching, which can be useful for building more complex navigation patterns.
  • Additionally, go_router provides a more elegant and declarative approach to routing, making it easier to manage the routing logic in your app. This can help to make your code more organized and maintainable, especially as your app grows in complexity.
  • So, if you need advanced routing features and a more elegant routing solution, you may consider using go_router. But if your navigation needs are simple, Navigator may be a more appropriate choice.

There are several advantages of using go_router in Flutter:

  • Named routes: go_router supports named routes, which makes it easier to manage and organize the navigation logic in your app. Named routes allow you to define the routes in a central location and then reference them throughout your app, making it easier to make changes to your navigation logic in the future.
  • Path parameters: go_router supports path parameters, which allow you to pass data to your routes based on the URL. This is useful for building dynamic navigation patterns, such as when you need to pass an ID or other data to a route.
  • Query parameters: go_router also supports query parameters, which allow you to pass data to your routes through the URL query string. This is useful for cases where you need to pass data that is not part of the route path.
  • Wildcard matching: go_router supports wildcard matching, which makes it easy to define catch-all routes that can handle any URL that doesn't match another route.
  • Declarative routing: go_router provides a more elegant and declarative approach to routing, making it easier to manage the routing logic in your app. This can help to make your code more organized and maintainable, especially as your app grows in complexity.
  • Custom routing transitions: go_router allows you to customize the routing transitions and animations, making it possible to create custom and unique navigation experiences in your app.

Overall, go_router provides a number of advanced routing features that can make it easier to manage and organize the navigation logic in your Flutter app. If your navigation needs are complex, go_router may be a good choice for your project.

Let's see an example of how we can structure our app:

go_router example graph

static GoRouter onBoardingRouter(AuthState authState) => GoRouter(
        initialLocation: "/logIn",
        navigatorKey: onBoardingNavigatorKey,
        routes: [
          GoRoute(
            name: "login",
            path: "/logIn",
            builder: (context, state) => const LoginPage(),
            //Example of redirection        
            redirect: (context, state) => authState == AuthState.loading ? "/splash" : null,
          ),
          GoRoute(
            name: "splash",
            path: "/splash",
            builder: (context, state) => const SplashPage(),
          ),
          GoRoute(
            name: "signUp",
            path: "/signUp",
            builder: (context, state) => const SignUpPage(),
          ),
          GoRoute(
            name: "passwordReset",
            path: "/passwordReset",
            builder: (context, state) => const ResetPasswordPage(),
          ),
        ],
      );


static GoRouter mainRouter = GoRouter(
    initialLocation: "/main",
    routes: [
      ShellRoute(
        navigatorKey: mainNavigatorKey,
        builder: (context, state, child) {
          return Cookies(
            child: HomeCorePage(
              child: child,
            ),
          );
        },
        routes: [
          GoRoute(
            name: "main",
            path: "/main",
            builder: (context, state) => const HomePage(),
            routes: [
              GoRoute(
                name: "postDetails",
                path: "postDetails",
                builder: (context, state) =>
                    PostDetailsPage(postId: state.params['id'] ?? ""),
              ),
              GoRoute(
                name: "chat",
                path: "chat",
                builder: (context, state) => ChatPage(),
              ),
              //...........
            ],
          ),
        ],
      ),
      ShellRoute(
        navigatorKey: mainNavigatorKey,
        builder: (context, state, child) {
          return Cookies(
            child: SettingsCorePage(
              child: child,
            ),
          );
        },
        routes: [
          GoRoute(
            name: "settings",
            path: "/settings",
            builder: (context, state) => const HomePage(),
            routes: [
              GoRoute(
                name: "profileEdit",
                path: "profile",
                builder: (context, state) => const EditProfilePage(),
                routes: [
                  GoRoute(
                    name: "resetPassword",
                    path: "resetPassword",
                    builder: (context, state) => const ResetPasswordPage(),
                  ),
                ],
              ),
              GoRoute(
                name: "themeSetup",
                path: "theme",
                builder: (context, state) => const ResetPasswordPage(),
              ),
              //...........
            ],
          ),
        ],
      ),
    ],
  );
Enter fullscreen mode Exit fullscreen mode

Go_router manages different types of routes:
ShellRoute: is a container, basically, all the child routes will be rendered inside of its widget.
i.e: Here HomeCorePage can have a container that renders a child widget with a classic nav drawer or a bottom nav bar to nav between screens, as usual when you change between screen we de HomeCorePage render again the new child and keeps the state of the rest of the screen.

static GoRoutermainRouter= GoRouter(
  initialLocation: "/main",
  routes: [
    // Home Navigation with drawer or bottom nav bar
    ShellRoute(
      navigatorKey: mainNavigatorKey,
      builder: (context, state, child) {
        return Cookies(
          child: HomeCorePage(
            child: child,
          ),
        );
      },
      routes: [
        GoRoute(
          name: "main",
          path: "/main",
          builder: (context, state) => const HomePage(),
        ),
        GoRoute(
          name: "settings",
          path: "/settings",
          builder: (context, state) => const SettingsPage(),
        ),
      ],
    ),
  ],
);
Enter fullscreen mode Exit fullscreen mode

GoRoute: the main route type, this is where you declare your route with the widget to render, and can have different children, let's see:

GoRoute(
          name: "main",
          path: "/main",
          builder: (context, state) => const HomePage(),
          redirect: (context, state) {
                if (!isUserAuthTo("main"))
                         return "/unauth";
                if (!state.params.containsKey("id"))
                      return "/main"
                    // return null to continue to the sub route
                return null;
          },
          routes: [
            GoRoute(
                name: "details",
                    path: "/details/:id",
                    builder: (context, state) => const Details(id: state.params["id"] ?? ""),
                ),
),
@amaury901130

Enter fullscreen mode Exit fullscreen mode

Here we can see different things:
Every route can have N sub-routes.
We can add validations before nav to a sub-route and redirect the user to any other page that we need in case don't match the validation.
Another advantage of go_router is that we can manage nav errors easily in the GoRouter class:

GoRouter(
   // when trying to nav to a missing route.
      errorBuilder: (context, state) {
          return ErrorPage(ErrorCode.e404);
      },
      initialLocation: "/main",
      routes: ....
)
Enter fullscreen mode Exit fullscreen mode

Deeplinks:
When a deep link is received from the platform, GoRouter will display the configured screen based on the URL path. To configure your Android or iOS app for deep linking, see the Deep linking documentation at flutter.dev:

Enable deep linking on Android
Add a metadata tag and intent filter to AndroidManifest.xml inside the tag with the ".MainActivity" name:

<!-- Deep linking -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="http" android:host="flutterbooksample.com" />
    <data android:scheme="https" />
</intent-filter>
Enter fullscreen mode Exit fullscreen mode

A full restart is required to apply these changes.
Test on Android emulator
To test with an Android emulator, give the adb command an intent where the host name matches the name defined in AndroidManifest.xml:

adb shell 'am start -a android.intent.action.VIEW \\ -c android.intent.category.BROWSABLE \\ -d "<http://flutterbooksample.com/book/1>"' \\ <package name>

Replace the <package name> with the package name of your Android app. If you named the package com.example.myflutterapp, run the following command:

adb shell 'am start -a android.intent.action.VIEW \\ -c android.intent.category.BROWSABLE \\ -d "<http://flutterbooksample.com/book/1>"' \\ com.example.myflutterapp
Enter fullscreen mode Exit fullscreen mode

For more details, see the Verify Android App Links documentation in the Android docs.

Enable deep linking on iOS
Add two new keys to Info.plist in the ios/Runner directory:

<key>FlutterDeepLinkingEnabled</key> 
<true/> 
<key>CFBundleURLTypes</key> 
<array> 
    <dict> 
    <key>CFBundleTypeRole</key> 
    <string>Editor</string> 
    <key>CFBundleURLName</key> 
    <string>flutterbooksample.com</string> 
    <key>CFBundleURLSchemes</key> 
    <array> 
    <string>customscheme</string> 
    </array> 
    </dict>
</array>
Enter fullscreen mode Exit fullscreen mode

The CFBundleURLName is a unique URL used to distinguish your app from others that use the same scheme. The scheme (customscheme://) can also be unique.
A full restart is required to apply these changes.
Test on iOS simulator
Use the xcrun command to test on the iOS Simulator:
xcrun simctl openurl booted
customscheme://flutterbooksample.com/book/1

In conclusion
Go_router is an alternative to Navigator that offers some advantages over the latter, especially in large and complex applications. We strongly recommend that if you're working on a complex application and need advanced navigation management, you should consider using go_router.
Below are some reasons why we should consider using go_router instead of Navigator:

  • Greater flexibility in defining routes: Go_router allows for a more flexible definition of navigation routes than Navigator. With go_router, we can define routes based on any criteria, such as the access route, URL parameters, application state, etc.
  • Better navigation management: Go_router offers more advanced navigation management than Navigator. For example, go_router allows us to define multiple navigation stacks for different areas of the application, making it easier to navigate between them without losing context.
  • Integration with the BLoC pattern: Go_router integrates well with the Business Logic Component (BLoC) pattern, which is a popular architecture pattern in Flutter. Go_router allows us to define routes that are directly linked to BLoC events, making it easier to manage navigation and application state.
  • Better performance: Go_router offers performance improvements over Navigator in large and complex applications. Go_router uses a tree-based data structure to store navigation routes, allowing for faster and more efficient route lookup.

Oldest comments (1)

Collapse
 
devlonoah profile image
Opeyemi Noah • Edited

Great article 🔥.

Flutter navigation is becoming more complex since majority of courses and tutorials are taught using Navigator approach.

UPDATE:
After spending 2 days fine-tuning navigation flow on a project I'm working on, All I can say is GoRouter is very powerful, reading documentation and guides made the integration seamless.

There is more functionality to discover thou.