zenrouter 3.0.0-beta.1 copy "zenrouter: ^3.0.0-beta.1" to clipboard
zenrouter: ^3.0.0-beta.1 copied to clipboard

Flutter router. Screens are types; the app is a graph. Deep links, web, and a visual route graph.

ZenRouter Logo

Type-safe navigation for Flutter apps.

pub package Test codecov


Define each screen as a type and compose routes as a graph. That gives you type-safe navigation, deep linking, browser back-button support, and a clear view of how screens connect.

Install #

3.0 is a prerelease. flutter pub add zenrouter still resolves 2.x.

dependencies:
  zenrouter: ^3.0.0-beta.1

Which style #

Need deep linking, URL sync, or the browser back button?
│
├─ YES → Coordinator
│
└─ NO → Is the stack derived from state?
       │
       ├─ YES → Declarative
       │
       └─ NO  → Imperative
Imperative Declarative Coordinator
Control path.push / pop Rebuild a route list coordinator.push / URI
Web / deep links Yes

Imperative #

final path = NavigationPath<AppRoute>.create();

NavigationStack(
  path: path,
  resolver: (route) => StackTransition.material(route.build(context)),
);

path.push(ProfileRoute());
path.pop();

Example · Guide

Declarative #

NavigationStack.declarative diffs the list (Myers) and applies the minimum push/pop set. Override props on parameterized routes.

NavigationStack.declarative(
  routes: [
    for (final page in pages) PageRoute(page),
  ],
  resolver: (route) => StackTransition.material(...),
);

Example · Guide

Coordinator #

Coordinator is RouterConfig. Routes mix RouteUnique and implement toUri(). parseRouteFromUri turns a URI into a route.

abstract class AppRoute extends RouteTarget with RouteUnique {}

class HomeRoute extends AppRoute {
  @override
  Uri toUri() => Uri.parse('/');

  @override
  Widget build(covariant AppCoordinator coordinator, BuildContext context) {
    return Scaffold(
      body: ListTile(
        title: const Text('Product 42'),
        onTap: () => coordinator.push(ProductRoute(id: '42')),
      ),
    );
  }
}

class AppCoordinator extends Coordinator<AppRoute> {
  @override
  AppRoute parseRouteFromUri(Uri uri) {
    return switch (uri.pathSegments) {
      [] => HomeRoute(),
      ['products', final id] => ProductRoute(id: id),
      _ => NotFoundRoute(uri),
    };
  }
}

MaterialApp.router(routerConfig: AppCoordinator())
coordinator.push(ProductRoute(id: '42'));
coordinator.navigate(HomeRoute());
coordinator.replace(HomeRoute());
coordinator.pop();
await coordinator.pushUri(Uri.parse('/products/42'));
Method Behavior
push Push onto the resolved stack
navigate Pop to an existing equal route, or push
replace Reset to a single route
recover / recoverUri Deep-link entry (RouteDeepLink)

build is typed to your coordinator subclass.

Example · Coordinator guide

New Coordinator apps should declare a RouteManifest instead of growing that switch.

Layouts #

A RouteLayout owns a StackPath. Bind the constructor on the path and set layout on children.

late final shopStack = NavigationPath<AppRoute>.createWith(
  label: 'shop',
  coordinator: this,
)..bindLayout(ShopLayout.new);

class ShopHomeRoute extends AppRoute {
  @override
  Type? get layout => ShopLayout;
}
Path Use
NavigationPath Nested stack
IndexedStackPath Tabs
BranchedStackPath Tabs that keep their own stacks

Layouts

Route mixins #

Mixin Role
RouteUnique URI identity; required on Coordinator routes
RouteLayout Shell; resolvePath + buildPath
RouteGuard popGuard / popGuardWith
RouteRedirect Replace the route before it is pushed
RouteDeepLink Deep-link strategy
RouteTransition Per-route page transition
RouteRestorable State after process death
RouteNotFound Keep the requested URI; resolve as 404

DevTools #

flutter pub add zenrouter_devtools
class AppCoordinator extends Coordinator<AppRoute>
    with CoordinatorDebug<AppRoute> {}

The overlay inspects stacks and can push a URI. Off in release (debugEnabled defaults to kDebugMode).

RouteManifest #

The recommended way to declare Coordinator routes in 3.0.

A parseRouteFromUri switch and each route's toUri() are two copies of the same paths. They drift. Overlaps (/products/new vs /products/:id) fail when a user hits the link, not when you ship. Layouts have no check that the shell in code matches the URL tree.

RouteManifest is the URI graph. RouteBinding is the only place that constructs a RouteTarget. Matching, reverse URLs, and the DevTools Graph tab all read that graph.

Benefit What it replaces
One pattern for match and location() switch + Uri.parse in every toUri()
Invalid graph fails at startup Broken link found in production
Typed pathParameters / restParameters Manual uri.pathSegments indexing
Feature fragments compose into one graph One giant parser
Graph tab (topology + observed flow) Guessing the tree from stack dumps

parseRouteFromUri still works. Use a manifest for new graphs.

class AppCoordinator extends Coordinator<AppRoute>
    with RouteModuleBinding<AppRoute, AppRouteId> {
  static final manifest = RouteManifest<AppRouteId>(
    name: 'app',
    idCodec: RouteIdCodec.enumValues(AppRouteId.values),
    routes: [
      RouteManifestRoute(id: AppRouteId.home, path: '/'),
      RouteManifestRoute(id: AppRouteId.product, path: '/products/:id'),
    ],
  );

  @override
  late final routeBindings = manifest.bind<AppRoute>(
    bindings: [
      RouteBinding(id: AppRouteId.home, create: (_) => HomeRoute()),
      RouteBinding(
        id: AppRouteId.product,
        create: (match) => ProductRoute(id: match.pathParameters['id']!),
      ),
    ],
    notFound: NotFoundRoute.new,
  );
}

The manifest has no widgets. RouteBinding is the factory from a match to a screen. RouteModuleBinding implements parseRouteFromUri. Bind every RouteManifestRoute; do not bind layout IDs.

Uri toUri() => AppCoordinator.manifest.location(
  AppRouteId.product,
  pathParameters: {'id': id},
);

:id is one segment. ...:slugs is a catch-all (match.restParameters['slugs']). Query strings are not part of the pattern.

With a manifest, also declare layouts as RouteManifestLayout and set parentId on children. A non-empty routeManifest enables the Graph tab in DevTools (topology + observed flow).

zenrouter_file_generator emits a coordinator, manifest, and bindings from lib/routes/.

Getting Started · Manifest example

Migrating from 2.x #

Apps that extend Coordinator still compile. parseRouteFromUri is unchanged.

Deprecated Replacement
defineLayout() NavigationPath.createWith(...)..bindLayout(ShopLayout.new)
defineConverter() defineRestorableConverter(...) in init()
createLayout / resolveLayout createParentLayout / resolveParentLayout

Migration guide

Documentation #

Packages #

Package Role
zenrouter Flutter Coordinator, NavigationStack, restoration
zenrouter_core RouteTarget, CoordinatorCore, paths, mixins, RouteManifest
zenrouter_devtools Overlay, Graph tab
zenrouter_file_generator File-based codegen

License #

Apache 2.0. LICENSE

definev

98
likes
160
points
12.1k
downloads
screenshot

Documentation

Documentation
API reference

Publisher

verified publisherzennn.dev

Weekly Downloads

Flutter router. Screens are types; the app is a graph. Deep links, web, and a visual route graph.

Homepage
Repository (GitHub)
View/report issues

Topics

#router #navigation #zenrouter #state-restoration #deep-linking

License

Apache-2.0 (license)

Dependencies

collection, flutter, zenrouter_core

More

Packages that depend on zenrouter