Navigation Utils
The missing navigation library for Navigator 2.
NavigationUtils simplifies the process of integrating Flutter's Navigator 2 into your applications.
Features
- Reimplements the Navigator 1 API in Navigator 2, including
push()
,pop()
,pushAndReplace()
and more. - Full control over the navigation back stack through
set()
. - Named route support with
pushNamed()
,setNamed()
. - Path-based routing support.
- Convenient functions for setting the URL and query parameters.
Should I use Navigation Utils?
You should use NavigationUtils if the existing Navigation Libraries aren't working for you and you need full control.
- ❌ DON'T USE NavigationUtils if an existing navigation library works for you (GoRouter, RouteMaster, etc).
- ✅ USE NavigationUtils if you're thinking about implementing Navigator 2 directly or writing your own Navigation library.
Here's a helpful diagram for deciding whether NavigationUtils is the right fit for a project.
NavigationUtils does NOT add complexity. Instead, it embraces the intricacies of Navigator 2, providing a nuanced, comprehensive approach to implementing navigation.
A few compelling arguments for using Navigation Utils:
- You're learning how to use Flutter's Navigator 2, not a third party library. The time you invest won't be wasted.
- You can implement complex navigation schemas without wrestling with the library.
- You can design navigation that seamlessly aligns with your app and architecture, rather than allowing navigation to dictate architectural choices.
As you incorporate more advanced navigation features, like deeplinks, authentication, and URLs, you'll likely encounter growing challenges and limitations that can drive up costs. There comes a point when direct, hands-on experience with Navigator 2 becomes crucial.
If navigation hurdles have become a constant in your development process, it's time to bite the bullet and master Navigator 2. The learning curve is steep, but the alternative is a seemingly endless cycle of issues, roadblocks, and limitations.
Quickstart
Initial App Routing Configuration
MaterialApp.router(
title: 'Navigation Utils Demo',
routerDelegate: NavigationManager.instance.routerDelegate,
routeInformationParser: NavigationManager.instance.routeInformationParser,
);
Tip: Navigator 2 utilizes MaterialApp.router
and requires a RouterDelegate
and RouteInformationParser
. These components replace the routes
and onGenerateRoute
builders of Navigator 1.
The NavigationManager acts as a global singleton, serving as a dependency injector while holding references to the RouterDelegate
and RouteInformationParser
. See the customization section for more information on how to use your own dependency injection and custom navigation lifecycle management.
Initialize NavigationManager
void main() {
NavigationManager.init(
mainRouterDelegate: DefaultRouterDelegate(navigationDataRoutes: routes),
routeInformationParser: DefaultRouteInformationParser());
runApp(const MyApp());
}
Tip: DefaultRouterDelegate
and DefaultRouteInformationParser
are convenience classes provided by this library to help you get up and running quickly. For more information on migrating an existing delegate or using a custom implementation, see the customization section.
Define Routes
List<NavigationData> routes = [
NavigationData(
url: '/',
builder: (context, routeData, globalData) =>
const MyHomePage()),
NavigationData(
label: ProjectsPage.name,
url: '/projects',
builder: (context, routeData, globalData) =>
const ProjectsPage()),
];
Note: Each route requires a URL because NavigationData
maps a URL to a specific page. The NavigationData model holds routing information that Flutter's navigator needs. For more insights on passing query parameters and using page constructors, see customization sections below.
NavigationData
contains an optional label
property to support named routing like in Navigator 1. Navigator 2 does not supported named routing out of the box so named routing is reimplemented. Here, ProjectsPage.name
is a static constant defined in the ProjectPage widget.
class ProjectsPage extends StatefulWidget {
static const String name = 'projects';
@override
_ProjectsPageState createState() => _ProjectsPageState();
}
Navigation
The NavigationData class in the NavigationUtils library is used to encapsulate all the necessary data for defining a route in your application. It provides an easy way to define and manage your routes.
NavigationData(
label: ProjectPage.name,
url: '/project',
builder: (context, routeData, globalData) =>
const ProjectPage(),
),
label
: An optionalString
for named navigation.url
: AString
that represents the URL for the route. This is used to match the incoming route. It must start with a '/'.builder
: ANavigationPageFactory
object. It is a function that returns aPage
widget. This builder is used to construct the page when the route is navigated to.pageType
: An optionalPageType
enum that can be used to further customize the type of the page. The PageType can bematerial
,cupertino
, ortransparent
.fullScreenDialog
: An optionalbool
that indicates whether the route is a full-screen modal dialog.barrierColor
: An optionalColor
that specifies the color of the barrier that will appear behind the dialog. This is used only iffullScreenDialog
istrue
.metadata
: AMap<String, dynamic>
that can hold any additional data you want to associate with the route.
Usage
NavigationUtils supports path, name, and Route object-based routing. You can directly access these navigation functions through NavigationManager.instance
.
Path
Path-based routing can be considered "absolute" routing as each URL path is unique. The path is also the URL shown in the address bar on Web.
NavigationManager.instance.push('/projects');
Named
Navigator 1's named route navigation. The name of the route is often defined in the respective page or component and used as a reference for navigation.
NavigationManager.instance.push(ProjectsPage.name);
Route Object
Navigation can also use the raw Route object. Here, a DefaultRoute object is created with the specified path, which is then passed to the navigation. This method is primarily used internally and for supporting partial migrations to this library.
NavigationManager.instance.pushRoute(DefaultRoute(path: '/projects'));
Routing Parameters
Navigator 2 does not support query parameters, path parameters, route guards, or non-serializable objects out of the box. The default PageRoute
class only supports URLs and arguments.
Please read and understand the following information as it is crucial to understanding how Navigator 2 works.
Important:
- Arguments are NOT query parameters or related to URL routing in any way. Arguments are an internal parameter used to pass data between pages from the Legacy Navigator 1 implementation.
- Navigator 2 uses the full URL string as a unique page identifier. By default it does not do any path processing or conform to expected URL routing logic or behavior.
/home
and/home/
are treated as two distinct pages.
Navigator 2's default URL handling behavior is very limited and wrong by default for web. NavigationUtils adds support for URL routing parameters by extending PageRoute
with a DefaultRoute
and building an abstraction layer called NavigationData
on top.
Query Parameters
Access query parameters via routeData.queryParameters
in NavigationData
. Query parameters are stored in a Map<String, String>
where the key is the query parameter name and the value is the query parameter value.
Example
// Route Definition
NavigationData(
label: ProjectPage.name,
url: '/project',
builder: (context, routeData, globalData) => ProjectPage(
id: int.tryParse(routeData.queryParameters['id'] ?? ''),
),
)
// Route Navigation
NavigationManager.instance.push(ProjectPage.name);
NavigationManager.instance.push(ProjectPage.name, queryParameters: {'id': '320'});
NavigationManager.instance.push('/project');
NavigationManager.instance.push('/project', queryParameters: {'id': '320'});
ProjectPage
is mapped to the URL ('/project'
). An id
query parameter is used to pass the ID of the project.
Note: All URL parameters are passed as Strings. This is because URLs are not "typed" and Strings by default.
- Extract
ints
anddoubles
withint.tryParse
anddouble.tryParse
. - Extract
bools
withrouteData.queryParameters[variable] == 'true'
where the value passed in the URL is atrue
orfalse
String.
Implementation Details
Navigator 2 does not support query parameters "out of the box" as the Navigator 2 API does not have a query parameter
field. By default, Navigator 2 treats query parameters as part of the URL string and different query parameters as unique pages.
For example, all of the below home /
URLs are treated as different pages by Navigator 2:
/
/?tab=community_page
/?tab=community_page&post=80
/?tab=message_page
/?referrer=google_ads
This is a problem because all of the URLs should point to the same page and query parameters should be passed to that page. To support query parameters properly, this library strips query parameters from URLs, stores them, and then rebundles them during the route construction process.
Internally, this library extracts the query parameters (tab
) and stores it in the constructed DefaultRoute
object, passing only the root /
URL to the underlying Navigator 2 API.
Multiple NavigationData
instances can be defined with different query parameters to handle various scenarios or variations of the same page, all pointing to the same destination.
Path Parameters
Path parameters are used to capture dynamic parts of a URL's path. They are denoted by a colon (:
) followed by a parameter name in the URL pattern. The corresponding values for each path parameter are extracted from the actual URL when a match is found.
Access path parameters via routeData.pathParameters
in NavigationData
. Path parameters are stored in a Map<String, String>
where the key is the path parameter name and the value is the path parameter value.
Example
// Route definition
NavigationData(
label: ProjectPage.name,
url: '/project/:projectId',
builder: (context, routeData, globalData) => ProjectPage(
id: int.tryParse(routeData.pathParameters['projectId'] ?? ''),
);
},
)
// Route navigation
NavigationManager.instance.push(ProjectPage.name);
NavigationManager.instance.push(ProjectPage.name, pathParameters: {'projectId': 320});
NavigationManager.instance.push('/project/320');
# Invalid: NavigationManager.instance.push('/project'); /project and /project/320 are different URLs.
In the example above, the ProjectPage
is associated with the URL pattern '/project/:projectId'
. The value of projectId
is extracted from the actual URL. These parameters are then used to construct the ProjectPage
with the corresponding values.
Multiple NavigationData
instances can be defined with different URL patterns and path parameters to handle various routes and dynamic parts of the URL path.
Note: Ensure that the URL patterns in the NavigationData
instances match the actual URLs accurately to enable correct parameter extraction.
/project
and/project/:projectId
are different URLs. To support both, define aNavigationData(url: '/project')
andNavigationData(url: '/project/:projectId')
.- A trailing slash such as
/project/
does not pass a null ID to/project/:projectId
. Instead,/project/
is equivalent to/project
.
Deeplinks
A special feature of NavigationUtils is it supports deeplinks as data and defining them all in a single list. This is done by creating a list of DeeplinkDestination
instances.
List<DeeplinkDestination> deeplinkDestinations = [
DeeplinkDestination(
deeplinkUrl: '/deeplink/login',
destinationLabel: LoginPage.name),
DeeplinkDestination(
deeplinkUrl: '/deeplink/signup',
destinationLabel: SignUpPage.name),
Each DeeplinkDestination
represents a unique deeplink within your application and includes properties such as deeplinkUrl
, destinationLabel
, and destinationUrl
to define the behavior of the deeplink.
This approach offers several advantages:
- Centralization: By defining all deeplinks in one place, it becomes easier to manage and update them. You can quickly find, add, remove, or modify deeplinks as your application evolves.
- Consistency: Having a single list ensures that every deeplink is defined in a consistent way, making your codebase more maintainable.
- Flexibility: Since deeplinks are defined as data, you can dynamically generate, modify, or filter them based on your application's needs.
Custom Deeplinks Behavior
DeeplinkDestination(
deeplinkUrl: '/deeplink/login',
destinationLabel: LoginPage.name,
destinationUrl: '/login',
backstack: [InitializationPage.name, StartPage.name],
backstackRoutes: [InitializationRoute(), StartRoute()],
excludeDeeplinkNavigationPages: [ForgotPassword.name],
shouldNavigateDeeplinkFunction: () {
if (AuthService.instance.isAuthenticated) return false;
return true;
},
mapArgumentsFunction: (pathParameters, queryParameters) {
// Remap or process path and query parameters.
String referrerId = queryParameters['referrer'] ?? '';
InstallReferrer.instance.setReferrerId(referrerId);
return {'id': pathParameters['userId'] ?? ''};
},
authenticationRequired: false,
)
deeplinkUrl
: A required property representing the deep link URL.destinationLabel
: The named route destination of the deep link.destinationUrl
: The URL route of the destination.backstack
andbackstackRoutes
: Specify the route backstack to which the user should return when navigating away from the deep link. Only one of these can be set.excludeDeeplinkNavigationPages
: A list of pages that should be excluded from deep link navigation.shouldNavigateDeeplinkFunction
: A function that determines whether the deep link should be navigated.mapPathParameterFunction
,mapQueryParameterFunction
,mapArgumentsFunction
,mapGlobalDataFunction
: Optional functions that map path parameters, query parameters, arguments, and global data, respectively.authenticationRequired
: A boolean indicating whether authentication is required to navigate the deeplink.
By providing these parameters, NavigationUtils gives you the flexibility to customize deeplink behavior to suit your application's specific needs. Contributors are welcome to open an issue and PR to add additional functionality that might be missing.
Deeplinks Usage
NavigationUtils includes a convenience function called NavigationUtils.openDeeplinkDestination
to process URIs and map them to deeplinks. Here is a sample implementation:
class DefaultRouteParser {
static bool openDeeplink(Uri? uri) {
return NavigationUtils.openDeeplinkDestination(
deeplinkDestinations: deeplinkDestinations,
routerDelegate: NavigationManager.instance.routerDelegate,
uri: uri,
authenticated: AuthService.instance.isAuthenticated,
currentRoute:
NavigationManager.instance.currentRoute,
excludeDeeplinkNavigationPages: doNotNavigateDeeplinkPages,
);
}
}
-
uri
: TheUri
object representing the deeplink that you want to open. -
deeplinkDestinations
: The list ofDeeplinkDestination
instances that define the deeplinks within your application. -
routerDelegate
: TheBaseRouterDelegate
instance that handles the actual navigation within your application. -
deeplinkDestination
: An optionalDeeplinkDestination
instance that you want to open. If not provided, the method will try to find the matching destination in thedeeplinkDestinations
list using theuri
. -
authenticated
: A boolean value that indicates whether the user is authenticated. This is used when theDeeplinkDestination
requires authentication. Defaults totrue
. -
currentRoute
: An optionalDefaultRoute
instance that represents the current route of the application. This is used for checking if the current page is in theexcludeDeeplinkNavigationPages
list. -
excludeDeeplinkNavigationPages
: A list of strings that represent the labels or paths of the routes that should be excluded from deeplink navigation. If the current route's label or path is in this list, the method will not perform the navigation. -
redirectFunction
: Redirect to another route.
This method tries to find the matching DeeplinkDestination
for the given uri
and performs various checks before navigating to the destination. These checks include checking whether the user is authenticated (if required), whether the current route is in the excluded list, and whether a custom navigation function allows the navigation. After these checks, the method navigates to the destination and processes any path parameters, query parameters, arguments, and global data as defined by the DeeplinkDestination
. Finally, the method updates the route stack using the routerDelegate
and applies the changes. The method returns true
if the navigation was successful, and false
otherwise.
Deeplink Redirect
Deeplinks can be redirected based on custom logic using the redirectFunction
. The redirectFunction
is used to handle deeplink redirections based on custom logic. It takes the current path and query parameters, applies the redirect logic, and determines whether to navigate to the original destination or to a different one.
Usage
Define the redirectFunction to specify the custom logic for redirections. The function is invoked with the current path and query parameters, along with a redirect callback to navigate to the new destination.
redirectFunction: (pathParameters, queryParameters, redirect) {
if (pathParameters.containsKey('id') && queryParameters.containsKey('action')) {
redirect(
label: 'newDestination',
pathParameters: {'id': pathParameters['id']!},
queryParameters: {'action': queryParameters['action']!},
globalData: {'additionalData': 'example'}
);
return Future.value(true);
}
return Future.value(false);
}
pathParameters
: AMap<String, String>
containing the current path parameters.queryParameters
: AMap<String, String>
containing the current query parameters.redirect
: The callback function to navigate to the new destination.
Return Value
The redirectFunction
returns a Future<bool>
. If the function returns true
, the redirection is considered successful, and the navigation proceeds to the new destination. If it returns false
, the navigation proceeds to the original destination.
URL Aliases
For single page apps, sometimes different URLs should map to the same Route. This fundamentally goes against Flutter's 1 to 1 URL to Route mapping. To solve this problem, the group
parameter in the NavigationData
class allows different URLs to map to the same page.
Usage
Define the group
parameter in NavigationData
to group multiple routes under the same destination.
NavigationData(
label: HomePage.name,
url: '/',
builder: (context, routeData, globalData) =>
HomePage(tab: routeData.queryParameters['tab'] ?? CommunityPage.name),
group: HomePage.name,
),
NavigationData(
label: CommunityPage.name,
url: '/community',
builder: (context, routeData, globalData) =>
HomePage(tab: CommunityPage.name),
group: HomePage.name,
),
NavigationData(
label: NewsPage.name,
url: '/news',
builder: (context, routeData, globalData) =>
HomePage(tab: NewsPage.name),
group: HomePage.name,
),
In this example, three different URLs ('/'
, '/community'
, and '/news'
) are mapped to the same HomePage
. This feature is particularly useful when you want multiple routes to lead to the same page while maintaining state and animations.
Route Guards
Authentication
NavigationUtils supports the common "Authenticated" route guard through the authenticationRequired
boolean.
- Annotate each
DeeplinkDestination
withauthenticationRequired
. - Pass the authentication state from your Authentication Service to
NavigationUtils.openDeeplinkDestination(authenticated: AuthService.instance.isAuthenticated)
.
Do Not Navigate Pages
When the user is on certain pages, such as the onboarding page, you may often want to disable deeplinks. NavigationUtils supports this behavior with excludeDeeplinkNavigationPages
.
- Define the list of pages to exclude in
excludeDeeplinkNavigationPages
. This list accepts named routes and path routes. - Pass the current page to
currentRoute
likecurrentRoute: NavigationManager.instance.currentRoute
.
Custom Route Guards
Setup Custom Route Guards by tagging NavigationData
routes with custom metadata
.
First, add custom tags to NavigationData(label: PremiumMemberPage.name, url: '/premium_page', metadata: {kUserStatus: PREMIUM})
. Here, the PremiumPage
is tagged with kUserStatus
and requires a PREMIUM
status to navigate.
if (NavigationManager.instance.routerDelegate
.currentConfiguration?.metadata?['kUserStatus'] == PREMIUM) {
DefaultRouteParser.openDeeplink(uri);
}
Async Route Guards
NavigationUtils supports route guards through the shouldNavigateDeeplinkFunction
property of the DeeplinkDestination
class. This function is called before navigating to the deeplink destination and can be used to prevent navigation based on certain conditions. For example, you can check if a user is authenticated before allowing navigation to a protected route.
Async Navigation
NavigationUtils supports asynchronous navigation, allowing you to perform asynchronous tasks such as data fetching or authentication checks before navigating to a deeplink destination. This is facilitated by the fact that the shouldNavigateDeeplinkFunction
can be an asynchronous function, meaning it can return a Future<bool>
instead of a simple bool
. This lets you perform any necessary async operations and delay navigation until those operations complete.