ark_navigation 1.2.1
ark_navigation: ^1.2.1 copied to clipboard
Typed modular Flutter navigation with explicit stack operations, deep links, nested scopes, and no code generation.
Ark Navigation #
Typed, modular navigation for Flutter applications, built directly on Flutter Router and Navigator.
Ark Navigation makes every destination and returned value part of a handwritten Dart contract. It does not require route generation, expose an untyped extra map, or hide stack mutations behind a generic navigation method.
Version 1.0 establishes the stable public API described in this guide. Breaking changes require a new major version.
Why Ark Navigation #
- Typed destinations, parameters, and route results.
- Explicit
push,replaceTop,replaceAll,pushAndRemoveUntil,replaceTail,popUntil, and branch operations. - Feature-owned modules that can be mounted into different applications without generated glue code.
- Separate Redirect and Guard contracts, including enter and leave Guard phases.
- Typed URI codecs, canonical links, decode-only aliases, and deterministic deep-link stack reconstruction.
- Nested Navigator scopes with explicit outlets and branch state.
- Browser-history intent attached to each operation.
- Restoration, dynamic graph updates, multiple windows, and separate-engine integration boundaries.
- No dependency on
go_router,auto_route, code generation, a service locator, or another ArkTelos package.
Installation #
Build applications with a patched stable SDK: Dart 3.13.7+ or Flutter 3.47.8+. Package SDK constraints describe API compatibility, not the security of older runtimes. After the eMudhra incident, affected applications must be rebuilt and redistributed; updating a library alone is not enough. See the SDK security guidance.
dependencies:
ark_navigation: ^1.2.1
Then import the public library:
import 'package:ark_navigation/ark_navigation.dart';
First route #
A destination is the typed request made by application code. Its type parameter is the value returned when that entry is popped.
final class HomeDestination extends NavigationDestination<void> {
const HomeDestination();
}
final class ProductDestination extends NavigationDestination<bool> {
const ProductDestination({required this.productId});
final String productId;
}
A route definition connects one destination type to a page and, for restorable routes, a URI codec:
final NavigationRouteDefinition<ProductDestination, bool> productRoute =
NavigationRouteDefinition<ProductDestination, bool>(
id: const NavigationRouteId('product'),
pageBuilder: (context, destination, entry) => ProductPage(
productId: destination.productId,
),
uriCodec: CallbackNavigationUriCodec<ProductDestination>(
pattern: '/products/:productId',
encoder: (destination) => Uri(
pathSegments: <String>['products', destination.productId],
),
decoder: (match) => ProductDestination(
productId: match.path('productId'),
),
),
);
Routes belong to a module. A mount places that module at a URI prefix and in a Navigator scope:
final NavigationGraph graph = NavigationGraph(
rootScopeId: const NavigationScopeId('root'),
scopes: const <NavigationScopeDefinition>[
NavigationScopeDefinition(
id: NavigationScopeId('root'),
restorationScopeId: 'root-navigation',
),
],
modules: <NavigationModule>[
NavigationModule(
id: const NavigationModuleId('catalog'),
routes: <NavigationRouteDefinitionBase>[homeRoute, productRoute],
),
],
mounts: <NavigationMount>[
NavigationMount(
id: const NavigationMountId('catalog-root'),
moduleId: const NavigationModuleId('catalog'),
scopeId: const NavigationScopeId('root'),
),
],
);
Create one session for the Flutter view and give its Router configuration to the application:
final NavigationSession session = NavigationSession(
id: const NavigationSessionId('main-window'),
graph: graph,
);
MaterialApp.router(routerConfig: session.routerConfig);
The session must be disposed by the object that owns it. NavigationRuntime is available when several views or windows share one graph.
Navigate and receive a typed result #
Application code should normally depend on the narrow NavigationPort contract. Inside the navigation tree it is available through BuildContext:
final NavigationTicket<bool> ticket = context.readNavigation.push<bool>(
const ProductDestination(productId: 'keyboard-42'),
);
final NavigationOperationResult commit = await ticket.committed;
final NavigationCompletion<bool> completion = await ticket.completed;
if (completion case NavigationPopped<bool>(:final result)) {
// result is bool?
}
Commit and completion are deliberately separate. A Guard can reject the request before an entry exists; a committed entry can later be popped with a typed result or removed by a stack rewrite.
Result typing is enforced at runtime as well as by Dart generics. pop rejects
a non-null value whose runtime type does not match the active Route Definition,
including values passed through dynamically typed integration code.
await context.readNavigation.pop(result: true);
Redirect and Guard #
A Redirect selects another typed target before Guards run. A Guard only permits or denies a transition:
final class AuthenticationRedirect implements NavigationRedirect {
const AuthenticationRedirect(this.session);
final SessionState session;
@override
NavigationTarget<Object?>? redirect(NavigationPolicyContext context) {
if (session.isSignedIn || context.target.destination is LoginDestination) {
return null;
}
return NavigationTarget<Object?>(
destination: LoginDestination(returnTo: context.target.destination),
);
}
}
final class UnsavedChangesGuard implements NavigationGuard {
const UnsavedChangesGuard(this.editor);
final EditorState editor;
@override
NavigationGuardDecision evaluate(NavigationPolicyContext context) {
if (context.phase == NavigationGuardPhase.leave && editor.hasChanges) {
return const NavigationGuardDenied(reason: 'unsaved_changes');
}
return const NavigationGuardAllowed();
}
}
Policies can be attached to the graph, module, mount, or route. Their dependencies are ordinary constructor parameters; the package does not prescribe a DI container.
Modules and nested navigation #
A feature exports a NavigationModule. The application composition root decides where that module is mounted. If one destination type is reachable through several mounts, the caller must select a NavigationMountId; Ark Navigation rejects ambiguous placement.
Each NavigationScopeDefinition owns an independent Navigator stack. Render a known child stack with NavigationOutlet, or the currently active child branch with NavigationActiveOutlet:
const NavigationActiveOutlet(
parentScopeId: NavigationScopeId('shell'),
)
Branch activation and reset are explicit operations:
await navigation.activateBranch(const NavigationScopeId('catalog-tab'));
await navigation.resetBranch(const NavigationScopeId('profile-tab'));
Deep links and restoration #
Restorable routes require a bidirectional NavigationUriCodec. Query parameters are read and written inside that typed codec rather than passed through the navigation API as a raw map. stackPlanBuilder can reconstruct the parent stack required by a directly opened URI.
Use NavigationRestorationPolicy.sessionOnly only for destinations that contain process-local values and cannot be reconstructed after a restart. Such a route may omit its URI codec.
Graph construction validates every canonical codec by encoding a representative Destination, matching the declared pattern, decoding it, and requiring the same canonical URI on re-encode. Empty route, module, mount, and scope identifiers are rejected before a session starts.
NavigationRuntime.updateGraph is atomic across all owned sessions. It first
prepares and validates every migration, then applies all of them; a failure
leaves every session on the previous graph. When a migration policy moves an
active entry out of a removed Scope, the entry is transferred to the selected
surviving Scope rather than left attached to a deleted Navigator.
Absolute platform links can be restricted before they reach the graph:
final NavigationPlatformLinkConfiguration platformLinks =
NavigationPlatformLinkConfiguration(
domains: <NavigationLinkDomain>[
NavigationLinkDomain(
scheme: 'https',
host: 'example.com',
pathPrefixes: const <String>['/app'],
),
],
);
Documentation #
Inspect a navigation request #
Attach an optional, bounded trace recorder when creating a Session:
final trace = NavigationTraceRecorder(capacity: 100);
final session = NavigationSession(
id: const NavigationSessionId('main-window'),
graph: graph,
eventSinks: <NavigationEventSink>[trace],
);
final steps = trace.steps;
final requestSteps = trace.forRequest(steps.last.requestId);
Each step says whether a request was made, redirected, rejected by a Guard, committed, or failed. Redirect and Guard steps include available route, mount, scope, phase, and type metadata. A commit includes revision and entry count. The buffer evicts oldest steps when full, so an older request may be partial. It does not retain Destination values, URIs, Guard reasons, exception objects, stack traces, or snapshots. Application-defined identifier values may still contain sensitive information; protect or clear the trace accordingly.
- Architecture and ownership
- Stack and branch operations
- Redirects, Guards, and policy refresh
- URI contracts and platform links
- Nested navigation and module composition
- Restoration and browser history
- Runtime, lifecycle, and dynamic graphs
- Migration from Navigator, go_router, and auto_route
- Runnable and focused examples
- Additive 1.x roadmap
AI agent skills #
The package ships two version-matched skills:
ark-navigation-route-implementationfor typed destinations, operations, Redirect, Guard, URI, results, and restoration;ark-navigation-modular-architecturefor graphs, modules, mounts, nested scopes, tabs, sessions, and runtime graph updates.
dart run skills@ get -p ark_navigation
Review installed instructions before use and keep the application's route and access policy in project-owned files.