NavigationUtils는 애플리케이션에 Navigator 2.0을 통합하는 것을 간소화하기 위해 설계된 가벼운 Flutter 패키지입니다. 현대적인 Flutter 앱에서 라우트 및 탐색 관리를 위한 깔끔하고 직관적인 API를 제공하여 반복 코드를 줄이고 개발자 경험을 향상시킵니다. 유연성을 고려하여 제작되었으며, Android, iOS, Web, macOS, Windows, Linux 등 모든 주요 플랫폼을 지원합니다. 이 패키지는 Flutter의 기본 탐색 시스템의 부족을 보완하여 라우트 관리를 더 예측 가능하고 확장 가능하게 만듭니다.
사용 사례
복잡한 앱 탐색 관리
딥링크 구현
경로 전환 간소화
동적 라우팅 지원
뒤로 버튼 처리 활성화
모듈형 탐색 흐름 만들기
주요 기능
나비게이터 2.0로 간편 설정
일관된 탐색 API
플랫폼 독립적 설계
내장된 라우트 수명 주기 관리
중첩된 네비게이터를 지원합니다
최소한의 보일러플레이트
적합한 대상
Navigator 2.0를 사용하는 개발자
고급 라우팅이 필요한 앱
확장 가능한 UI 아키텍처를 구축하는 팀
크로스 플랫폼 Flutter 프로젝트
명확한 탐색 패턴이 필요한 프로젝트
고려 사항
Navigator 2.0 개념을 이해해야 합니다
추가 상태 관리가 필요할 수 있습니다
간단한 단일 화면 앱에 적합하지 않습니다
핵심 플러터에 비해 문서가 제한적
구조화된 라우팅 접근 방식과 함께 사용하는 것이 가장 좋습니다
원본 README
연결된 소스 저장소의 README 원문 스냅샷입니다. 최신 내용은 저장소에서 확인하세요.
Navigation Utils
The missing navigation library for Flutter's Navigator 2.
NavigationUtils makes it easy to implement Navigator 2 in your Flutter app.
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.
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.
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.
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.
classProjectsPageextendsStatefulWidget{
staticconstString name = 'projects';
@override_ProjectsPageStatecreateState() => _ProjectsPageState();
}
Complete authentication flow with login/signup screens.
Route guards for authenticated pages.
Navigation sync with user auth.
Deep link and URL parameter handling.
Flutter Firebase Auth integration and issues solved.
Working Firebase Auth example on all platforms.
Handling navigation during authentication state changes.
Managing loading and initialization.
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.
url: A String that represents the URL for the route. This is used to match the incoming route. It must start with a '/'.
builder: A NavigationPageFactory object. It is a function that returns a Page widget. This builder is used to construct the page when the route is navigated to.
pageType: An optional PageType enum that can be used to further customize the type of the page. The PageType can be material, cupertino, or transparent.
fullScreenDialog: An optional bool that indicates whether the route is a full-screen modal dialog.
barrierColor: An optional Color that specifies the color of the barrier that will appear behind the dialog. This is used only if fullScreenDialog is true.
metadata: A Map<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.
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.
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.
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 and doubles with int.tryParse and double.tryParse.
Extract bools with routeData.queryParameters[variable] == 'true' where the value passed in the URL is a true or false 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:
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.
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 a NavigationData(url: '/project') and NavigationData(url: '/project/:projectId').
A trailing slash such as /project/ does not pass a null ID to /project/:projectId. Instead, /project/ is equivalent to /project.
Passing Objects and Data Between Pages with GlobalData
Arbitrary data such as classes and non-serializable variables can be passed between pages with globalData. globalData can be used to pass anything between pages.
A PostModel is passed to PostPage via globalData. The PostPage widget can now load the Post UI immediately.
Accessing and Modifying globalData
Beyond its use during navigation, globalData can be accessed and modified at any point in your application at NavigationManager.instance.routerDelegate.globalData. This allows for setting data and configurations at anytime.
Example:
// Set or update dataNavigationManager.instance.routerDelegate.globalData['selected_variant'] = 'A';
// Access dataString variant = NavigationManager.instance.routerDelegate.globalData['selected_variant'];
Note:globalData is not bound to the page lifecycle so any variables set must be manually disposed. Any outdated variables will need to be explicitly cleared. Most of the time, opening a page will set and override the data so stale variables are not a concern.
Note: The URL of the page is used as the key for storing data.
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.
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) returnfalse;
returntrue;
},
mapArgumentsFunction: (pathParameters, queryParameters) {
// Remap or process path and query parameters.String referrerId = queryParameters['referrer'] ?? '';
InstallReferrer.instance.setReferrerId(referrerId);
return {'id': pathParameters['userId'] ?? ''};
},
runFunction: (pathParameters, queryParameters) async {
// Arbitrary function call for handling deeplinks without doing navigation.
},
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 and backstackRoutes: 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.
runFunction: A function to support handling deeplinks without navigating, such as allowing deeplinks to trigger app functionality such as analytics, sharing data through social links, or showing a bottom sheet, without navigating to a new page. This function is also called after navigation completes, which allows for logic to be run after deeplink navigation is complete.
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:
uri: The Uri object representing the deeplink that you want to open.
deeplinkDestinations: The list of DeeplinkDestination instances that define the deeplinks within your application.
routerDelegate: The BaseRouterDelegate instance that handles the actual navigation within your application.
deeplinkDestination: An optional DeeplinkDestination instance that you want to open. If not provided, the method will try to find the matching destination in the deeplinkDestinations list using the uri.
authenticated: A boolean value that indicates whether the user is authenticated. This is used when the DeeplinkDestination requires authentication. Defaults to true.
currentRoute: An optional DefaultRoute instance that represents the current route of the application. This is used for checking if the current page is in the excludeDeeplinkNavigationPages 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.
pathParameters: A Map<String, String> containing the current path parameters.
queryParameters: A Map<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.
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.
Widget Reuse and Performance Optimization
Routes with the same group share the same widget instance, meaning the widget is reused rather than destroyed and recreated when navigating between grouped routes. This provides significant performance benefits:
Preserves widget state - The widget's state is maintained across navigation
Avoids rebuild overhead - Only didUpdateWidget is called instead of initState
Smooth animations - Transitions between grouped routes are seamless
Important: When grouping routes that reuse widgets:
Do NOT use const constructors - The const keyword prevents Flutter from detecting widget parameter changes. Use regular constructors to ensure didUpdateWidget is called properly.
Implement didUpdateWidget - Override this method in your StatefulWidget to handle parameter changes:
@overridevoiddidUpdateWidget(AuthPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.type != widget.type) {
setState(() {
// Update state when type changes
});
}
}
Tabbed interfaces where tabs share common UI elements
Any scenario where you want to preserve expensive widget state (like scroll position, form data, or animation controllers)
How This Works: Flutter Navigator 2 Internals
Flutter's Page Update Detection:
When Navigator 2 determines whether to update or recreate a Page's child widget, it uses Page.canUpdate:
canUpdate returns true - The Route is reused, and the child can receive didUpdateWidget
canUpdate returns false - A new Route is created, calling initState
For grouped routes:
All routes in the same group share the same Page key (the group name)
When canUpdate returns true (same key), the Route is reused
The Route's buildPage method reads _page.child at build time
Since _page (settings) now points to the new Page with a new child widget, the new child is returned
Flutter sees the child widget changed and calls didUpdateWidget
The Critical Mechanism:
The Route must read the child from settings (the Page) at build time, not capture it at creation time. This ensures that when navigating between grouped routes, the Route always uses the latest child widget from the Page settings, triggering didUpdateWidget as expected.
NavigationUtils supports the common "Authenticated" route guard through the authenticationRequired boolean.
Annotate each DeeplinkDestination with authenticationRequired.
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 like currentRoute: 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.
NavigationUtils allows you to customize route transitions both globally and on a per-page basis.
Global Transitions
You can define a global transition that will be applied to all routes unless overridden by a local (per-page) transition. To set a global transition, create a custom Page class with a custom Route that reads the child at build time.
⚠️ Important: Do NOT use PageRouteBuilder for custom transitions. It captures the child widget in a closure at Route creation time, which prevents page updates when using grouped routes or query parameter changes. Always create a custom Route class that reads _page.child at build time.
// Define a Custom Page for your global transitionclassScaleTransitionPageextendsPage<void> {
finalWidget child;
constScaleTransitionPage({
requiredthis.child,
super.key,
super.name,
super.arguments,
});
@overrideRoute<void> createRoute(BuildContext context) {
return_ScaleTransitionRoute(page: this);
}
}
// Custom Route that reads child at BUILD TIME (not creation time)class_ScaleTransitionRouteextendsPageRoute<void> {
_ScaleTransitionRoute({requiredScaleTransitionPage page})
: super(settings: page);
// Read from settings at build time - this is the key!ScaleTransitionPageget _page => settings asScaleTransitionPage;
@overrideColor? get barrierColor => null;
@overrideString? get barrierLabel => null;
@overrideboolget maintainState => true;
@overrideDurationget transitionDuration => constDuration(milliseconds: 300);
@overrideWidgetbuildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return _page.child; // Read child from CURRENT page at build time
}
@overrideWidgetbuildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
returnScaleTransition(
scale: animation,
alignment: Alignment.center,
child: child,
);
}
}
// Override pageBuilderNavigationManager.init(
mainRouterDelegate: DefaultRouterDelegate(
navigationDataRoutes: routes,
pageBuilder: ({
key,
name,
child,
routeData,
globalData,
arguments,
}) =>
ScaleTransitionPage(
key: key,
name: name,
arguments: arguments,
child: child,
),
),
routeInformationParser: DefaultRouteInformationParser(),
);
This will override the default MaterialPage transition animation and apply a scale transition to all pages. The custom Route class ensures that didUpdateWidget() is called correctly when the page updates.
Per-Page Transitions
To override the global transition for a specific route, create a custom Page class with its own Route and use the pageBuilder property in NavigationData:
You can create any custom transition effect you need by defining your own Page classes and using them either globally or on a per-page basis.
Understanding Cache Behavior
NavigationUtils implements intelligent page caching to optimize performance and provide smooth navigation experiences.
🎯 Unique Feature: This caching system is exclusive to NavigationUtils. Flutter's Navigator 2 and popular libraries like go_router have a bug which causes significant performance issues when there are 5+ pages in the navigation stack as all pages get rebuilt on every navigation event. This works by default in Navigator 1, but requires special care due to opaque Navigator internal equality checks. NavigationUtils brings back this essential optimization by intelligently caching and reusing page instances.
The caching system automatically handles:
Grouped routes that share widget instances (e.g., tab navigation)
Query parameter changes that update pages instead of recreating them
Duplicate routes that create separate instances when needed
State preservation across navigation events
For a comprehensive guide on how cache keys are generated and managed, see CACHE_BEHAVIOR.md.
Key Behaviors
Query Parameters Update Pages:
push('/product?id=1'); // Creates pagepush('/product?id=2'); // Updates same page with new data
Grouped Routes Share Instances:
NavigationData(url: '/', group: 'home'),
NavigationData(url: '/games', group: 'home'),
// Both share the same widget instance
Duplicates Create New Instances:
push('/item'); // First instancepush('/item'); // Second instance (separate page)
Enabling Hot Reload for Routes
When developing, you may add or change routes and expect hot reload to pick up the updates. Unfortunately, hot reload does not work out of the box with Flutter's Navigator widget if it is static, which it needs to be to avoid recreating itself on every navigation event. To enable hot reload, add the following to your top level App widget.