ark_navigation 0.1.0-dev.2 copy "ark_navigation: ^0.1.0-dev.2" to clipboard
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.

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'));

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 #

0
likes
160
points
0
downloads

Documentation

Documentation
API reference

Publisher

verified publisherarktelos.dev

Weekly Downloads

Typed modular Flutter navigation with explicit stack operations, deep links, nested scopes, and no code generation.

Homepage
Repository (GitLab)
View/report issues

Topics

#deep-linking #flutter #navigation #routing

License

Apache-2.0 (license)

Dependencies

flutter

More

Packages that depend on ark_navigation