ark_navigation 0.1.0-dev.2
ark_navigation: ^0.1.0-dev.2 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.
The package is currently a development preview. Its API may change before the first stable release.
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 #
dependencies:
ark_navigation: ^0.1.0-dev.2
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.
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.
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 #
- 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