DEV Community

Creating a Context-Free Navigation Function in Flutter

Navigating between screens is a fundamental aspect of mobile app development in Flutter. Typically, navigation relies on the BuildContext to move from one screen to another, but this can become cumbersome in complex applications.

When navigation needs to be triggered from non-widget classes, such as services or view models, accessing BuildContext can lead to tightly coupled and less maintainable code. As your app grows, managing navigation through context can clutter the codebase and make it harder to maintain, especially for background tasks or services that require navigation.

Image description

A context-free navigation approach offers a clean and efficient solution by decoupling navigation logic from the BuildContext, allowing greater flexibility and control over your appโ€™s navigation flow. While the go_router package is a powerful tool that provides a declarative approach to routing and deep linking, it still typically requires BuildContext for navigation actions.

In this article, we'll explore how to implement a basic global navigation system in Flutter by setting up a global navigator key and creating a dedicated navigation service. This method not only serves as a context-free navigation solution but also functions as a global context, simplifying the navigation process and enabling you to initiate navigation commands from anywhere in your app. By the end of this guide, you'll have a streamlined navigation architecture that enhances your development workflow and improves your appโ€™s user experience. Let's dive in and transform the way you handle navigation in your Flutter projects.

๐Ÿšจ The Problem with Context-Dependent Navigation: ๐Ÿšจ
Discuss common issues developers face with context-dependent navigation, such as:

  • Difficulty navigating from non-widget classes or services.
  • Complicated navigation flows where passing context around becomes cumbersome.
  • Situations where context is not immediately available, leading to convoluted code.

โ‡๏ธ The Solution: Context-Free Navigation:
Introduce the concept of context-free navigation and its advantages:

  • Simplifies navigation logic.
  • Enhances code readability and maintainability.
  • Enables navigation from any part of the app, including services, controllers, or background tasks.

Many of you might be using various packages for handling navigation in Flutter apps, such as go_router and get. These packages are powerful tools that help manage navigation efficiently. However, in this article, we will explore how to optimize Flutter's built-in navigation capabilities without relying on third-party plugins. This approach not only streamlines your app but also deepens your understanding of how Flutter's navigation system works.

Let's take a look the flutter basic navigation,

Image description

or

Image description

The examples above are powerful and sufficient for basic navigation. However, they are only applicable when navigating from a class or widget that has a BuildContext. There will be cases where you need to navigate from a controller that does not have a BuildContext. While it's possible to pass the BuildContext through functions, this approach can be cumbersome and require additional effort.


๐Ÿš€ So, let's start creating our own context-free navigation function.

class NavigationService {
  static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
}
Enter fullscreen mode Exit fullscreen mode

We'll create a class named NavigationService,

This GlobalKey is used to get a reference to the NavigatorState of the application. By using a global key, you can access the navigator from anywhere in your app.

Now let's add the functions into the NavigationService class.

push

Future<T?> push<T extends Object?>(Route<T> route) async {
  return navigatorKey.currentState?.push<T>(route);
}
Enter fullscreen mode Exit fullscreen mode

This method pushes a new route onto the navigator using a Route object. and route is the new route to push.

pushNamed

Future<T?> pushNamed<T extends Object?>(
  String routeName, {
  Object? arguments,
}) async {
  return navigatorKey.currentState?.pushNamed<T>(
    routeName,
    arguments: arguments,
  );
}
Enter fullscreen mode Exit fullscreen mode

This method pushes a new route onto the navigator using a named route.

  • routeName is the name of the route.
  • arguments is an optional parameter that allows passing data to the new route.

pushReplacementNamed

Future<T?> pushReplacementNamed<T extends Object?, TO extends Object?>(
  String routeName, {
  Object? arguments,
  TO? result,
}) async {
  return navigatorKey.currentState?.pushReplacementNamed<T, TO>(
    routeName,
    arguments: arguments,
    result: result,
  );
}
Enter fullscreen mode Exit fullscreen mode

This method replaces the current route with a new route using a named route.

  • routeName is the name of the new route.
  • arguments is an optional parameter to pass data to the new route.
  • result is an optional parameter to return a result to the previous route.

pushNamedAndRemoveUntil

Future<T?> pushNamedAndRemoveUntil<T extends Object?>(
  String routeName, {
  Object? arguments,
  bool Function(Route<dynamic>)? predicate,
}) async {
  return navigatorKey.currentState?.pushNamedAndRemoveUntil<T>(
    routeName,
    predicate ?? (_) => false,
    arguments: arguments,
  );
}
Enter fullscreen mode Exit fullscreen mode

This method pushes a new route and removes all previous routes until the predicate returns true.

  • routeName is the name of the new route.
  • arguments is an optional parameter to pass data to the new route.
  • predicate is a function that determines which routes to remove. If not provided, all previous routes are removed.

popUntil

void popUntil(String route) {
  navigatorKey.currentState?.popUntil(ModalRoute.withName(route));
}
Enter fullscreen mode Exit fullscreen mode

This method pops routes off the navigator until the specified route is reached.

  • route is the name of the route to pop until.

goBack

void goBack<T extends Object?>({T? result}) {
  navigatorKey.currentState?.pop<T>(result);
}
Enter fullscreen mode Exit fullscreen mode

This method pops the current route off the navigator.

  • result is an optional parameter to pass a result to the previous route.

The functions above are navigation functions commonly used in Flutter apps.
Now let's add one more step, โ˜๐Ÿป

Go to your MaterialApp and attach the NavigationService navigatorKey.

void main() {
  runApp(
    MaterialApp(
      navigatorKey: NavigationService.navigatorKey,
      home: HomePage(),
    ),
  );
}
Enter fullscreen mode Exit fullscreen mode

Now you can then use the NavigationService to perform navigation from anywhere in your app, without needing a BuildContext:

Here's the usage

NavigationService().pushNamed('/secondPage');
Enter fullscreen mode Exit fullscreen mode
NavigationService().push(
 MaterialPageRoute(builder: (context) => AnotherScreen()),
);
Enter fullscreen mode Exit fullscreen mode

๐Ÿ• Before Context-Free Navigation

Navigator.push(
  context, 
  MaterialPageRoute(
    builder: (builder) => const LoginPage()
  )
);

Navigator.pushNamed(
 context, '/foo', arguments: someObject
)
Enter fullscreen mode Exit fullscreen mode

โœ… After

NavigationService().push(
 MaterialPageRoute(builder: (context) => AnotherScreen()),
);

NavigationService().pushNamed('/secondPage', arguments: someObject);
Enter fullscreen mode Exit fullscreen mode

Now you're no longer need to pass BuildContext when navigating.


Conclusion

The NavigationService class provides a set of utility methods to handle navigation in a Flutter application without requiring direct access to a BuildContext. This is achieved using a global NavigatorState key. Below are the key functionalities provided by the class:

Global Navigation Key:

  • navigatorKey: A global key to access the navigator state.

Navigation Methods:

  • pushNamed: Navigates to a named route with optional arguments.
  • push: Pushes a route onto the navigator stack.
  • pushReplacementNamed: Replaces the current route with a named route.
  • pushNamedAndRemoveUntil: Pushes a named route and removes routes until the predicate returns true.
  • pushAndRemoveUntil: Pushes a route and removes routes until the predicate returns true.
  • goBack: Pops the top-most route off the navigator.
  • popUntil: Pops routes until the specified route is reached

Key Points:

  • Context-Free Navigation: Allows navigation operations without needing the BuildContext, useful for scenarios where the context isn't available.
  • Utility Methods: Provides comprehensive navigation methods, covering various use-cases such as pushing, replacing, and popping routes.
  • Global Accessibility: The global navigator key makes the navigator state accessible throughout the app, facilitating easy and centralized navigation handling.

By using NavigationService, developers can simplify and streamline navigation in their Flutter applications, especially in situations where direct access to the context is challenging. And by creating our own navigation, we can reduce the dependency on public plugins or packages, which may not be maintained by the publisher.


โœ… That's all!

I hope you find this guide useful and that it enhances your application! Thank you for reading, and happy coding!

Full Code:
GitHub

Top comments (0)