Installation
flutter pub add zenrouter
Architecture Overview
ZenRouter provides three navigation paradigms through a layered architecture:
RouteTarget (base class for all routes)
├── Imperative → NavigationPath + NavigationStack
├── Declarative → NavigationStack.declarative (Myers diff)
└── Coordinator → Coordinator<T> + MaterialApp.router
└── RouteUnique (URI-based identity for deep linking)
Class Architecture
| Component | Responsibility |
|---|---|
RouteTarget |
Base class for all routes; provides identity via props and lifecycle |
NavigationPath |
Mutable stack container supporting push, pop, replace, and reset |
NavigationStack |
Flutter widget that renders a NavigationPath as a Navigator |
Coordinator<T> |
Central navigation hub orchestrating URI parsing, deep linking, layout resolution, and platform integration |
Route Mixins
| Mixin | Responsibility |
|---|---|
RouteUnique |
Provides URI-based identity; required for Coordinator routes |
RouteGuard |
Prevents popping via popGuard() / popGuardWith(coordinator) |
RouteRedirect |
Resolves redirects before the route is pushed |
RouteDeepLink |
Customises how a route is restored from a deep link URI |
RouteLayout |
Declares nested layout hierarchy; resolves child StackPath |
RouteTransition |
Overrides the default page transition for a specific route |
RouteRestorable |
Enables state restoration after process death |
Paradigm Selection
Need deep linking, URL sync, or browser back button?
│
├─ YES → Coordinator
│
└─ NO → Is navigation derived from state?
│
├─ YES → Declarative
│
└─ NO → Imperative
| Imperative | Declarative | Coordinator | |
|---|---|---|---|
| Simplicity | ⭐⭐⭐ | ⭐⭐ | ⭐ |
| Web / Deep Linking | ❌ | ❌ | ✅ |
| State-Driven | Compatible | ✅ Native | Compatible |
| Route Mixins | Guard, Redirect, Transition | Guard, Redirect, Transition | Guard, Redirect, Transition, DeepLink |
Imperative
Direct stack control via NavigationPath. Routes are pushed and popped explicitly.
Role in Navigation Flow
- Create route classes extending
RouteTarget - Create a
NavigationPathto hold the route stack - Render the stack with
NavigationStack, providing aresolverthat maps each route to aStackTransition - Call
push()/pop()on the path to navigate
Example
import 'package:flutter/material.dart';
import 'package:zenrouter/zenrouter.dart';
// 1. Define routes
sealed class OnboardingRoute extends RouteTarget {
Widget build(BuildContext context);
}
class WelcomeStep extends OnboardingRoute {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => onboardingPath.push(
PersonalInfoStep(formData: const OnboardingFormData()),
),
child: const Text('Start Onboarding'),
),
),
);
}
}
class PersonalInfoStep extends OnboardingRoute with RouteGuard {
final OnboardingFormData formData;
PersonalInfoStep({required this.formData});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Personal Information')),
body: Center(
child: ElevatedButton(
onPressed: () => onboardingPath.push(
PreferencesStep(formData: formData.copyWith(fullName: 'Joe')),
),
child: const Text('Continue'),
),
),
);
}
@override
Future<bool> popGuard() async => true; // prevent accidental back
}
// 2. Create a NavigationPath
final onboardingPath = NavigationPath.create();
// 3. Wire up with NavigationStack
class AppRouter extends StatelessWidget {
@override
Widget build(BuildContext context) {
return NavigationStack(
path: onboardingPath,
resolver: (route) => switch (route) {
WelcomeStep() => StackTransition.material(route.build(context)),
PersonalInfoStep() => StackTransition.material(route.build(context)),
PreferencesStep() => StackTransition.material(route.build(context)),
},
);
}
}
Navigate with:
onboardingPath.push(PersonalInfoStep(formData: data));
onboardingPath.pop();
onboardingPath.reset();
Declarative
State-driven navigation. The route list is rebuilt from state and ZenRouter applies the Myers diff algorithm to compute minimal push/pop operations.
Role in Navigation Flow
- Define routes extending
RouteTarget - Use
NavigationStack.declarativewith arouteslist derived from state - When state changes, rebuild the
routeslist — ZenRouter diffs and applies the minimal set of changes
Example
import 'package:flutter/material.dart';
import 'package:zenrouter/zenrouter.dart';
class PageRoute extends RouteTarget {
final int pageNumber;
PageRoute(this.pageNumber);
@override
List<Object?> get props => [pageNumber];
}
class SpecialRoute extends RouteTarget {}
class DemoScreen extends StatefulWidget {
const DemoScreen({super.key});
@override
State<DemoScreen> createState() => _DemoScreenState();
}
class _DemoScreenState extends State<DemoScreen> {
final List<int> _pageNumbers = [1];
int _nextPageNumber = 2;
bool showSpecial = false;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: NavigationStack.declarative(
routes: <RouteTarget>[
for (final pageNumber in _pageNumbers) PageRoute(pageNumber),
if (showSpecial) SpecialRoute(),
],
resolver: (route) => switch (route) {
SpecialRoute() => StackTransition.sheet(SpecialPage()),
PageRoute(:final pageNumber) =>
StackTransition.material(PageView(pageNumber: pageNumber)),
_ => throw UnimplementedError(),
},
),
),
ElevatedButton(
onPressed: () => setState(() {
_pageNumbers.add(_nextPageNumber++);
}),
child: const Text('Add Page'),
),
],
),
);
}
}
Coordinator
Central navigation hub for deep linking, URL synchronisation, browser navigation, state restoration, and nested layouts.
Inheritance Architecture
Coordinator<T extends RouteUnique>
extends CoordinatorCore<T> // Push, pop, replace, navigate
with CoordinatorLayout<T> // Layout hierarchy resolution
, CoordinatorRestoration<T> // State restoration after process death
implements RouterConfig<RouteTarget> // Platform Router integration
Role in Navigation Flow
- Define route classes extending
RouteTarget with RouteUnique - Create a
Coordinator<T>subclass implementingparseRouteFromUri - Declare
NavigationPathandIndexedStackPathinstances for nested stacks - Wire up with
MaterialApp.routerusingrouterDelegateandrouteInformationParser - Navigate with
coordinator.push(),coordinator.pop(),coordinator.replace()
Example
import 'package:flutter/material.dart';
import 'package:zenrouter/zenrouter.dart';
// 1. Base route with RouteUnique (required for URI identity)
abstract class AppRoute extends RouteTarget with RouteUnique {}
// 2. Define routes
class FeedTab extends AppRoute {
@override
Uri toUri() => Uri.parse('/home/tabs/feed');
@override
Widget build(AppCoordinator coordinator, BuildContext context) {
return ListView(
children: [
ListTile(
title: const Text('Post 1'),
onTap: () => coordinator.push(FeedDetail(id: '1')),
),
],
);
}
}
class FeedDetail extends AppRoute with RouteGuard, RouteRedirect {
FeedDetail({required this.id});
final String id;
@override
List<Object?> get props => [id];
@override
Uri toUri() => Uri.parse('/home/feed/$id');
@override
AppRoute redirect() {
if (id == 'profile') return ProfileDetail();
return this;
}
@override
FutureOr<bool> popGuardWith(AppCoordinator coordinator) async {
final confirm = await showDialog<bool>(
context: coordinator.navigator.context,
builder: (context) => AlertDialog(
title: const Text('Leave this page?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('No'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Yes'),
),
],
),
);
return confirm ?? false;
}
@override
Widget build(AppCoordinator coordinator, BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Feed Detail $id')),
body: Center(
child: ElevatedButton(
onPressed: () => coordinator.pop(),
child: const Text('Go Back'),
),
),
);
}
}
// 3. Layout routes for nested navigation
class HomeLayout extends AppRoute with RouteLayout<AppRoute> {
@override
NavigationPath<AppRoute> resolvePath(AppCoordinator coordinator) =>
coordinator.homeStack;
@override
Widget build(AppCoordinator coordinator, BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: buildPath(coordinator), // renders nested NavigationPath
);
}
}
class TabBarLayout extends AppRoute with RouteLayout<AppRoute> {
@override
Type get layout => HomeLayout;
@override
IndexedStackPath<AppRoute> resolvePath(AppCoordinator coordinator) =>
coordinator.tabIndexed;
@override
Widget build(AppCoordinator coordinator, BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(child: buildPath(coordinator)),
// tab bar UI ...
],
),
);
}
}
// 4. Coordinator — the central hub
class AppCoordinator extends Coordinator<AppRoute> {
late final homeStack = NavigationPath.createWith(
label: 'home', coordinator: this,
)..bindLayout(HomeLayout.new);
late final tabIndexed = IndexedStackPath.createWith(
coordinator: this, label: 'home-tabs',
[FeedTab(), ProfileTab(), SettingsTab()],
)..bindLayout(TabBarLayout.new);
@override
List<StackPath> get paths => [...super.paths, homeStack, tabIndexed];
@override
AppRoute parseRouteFromUri(Uri uri) {
return switch (uri.pathSegments) {
[] => FeedTab(),
['home', 'tabs', 'feed'] => FeedTab(),
['home', 'feed', final id] => FeedDetail(id: id),
_ => NotFound(uri: uri),
};
}
}
// 5. Wire up with MaterialApp.router
class MyApp extends StatelessWidget {
static final coordinator = AppCoordinator();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
restorationScopeId: 'app', // enables state restoration
routerConfig: coordinator,
);
}
}
Important
The build() method on RouteUnique routes receives the concrete coordinator type (e.g. AppCoordinator), not the generic Coordinator. This is because Coordinator is covariant — giving you type-safe access to custom paths and methods.
Important
State restoration requires routes to be parsed synchronously during startup. If parseRouteFromUri is asynchronous, override parseRouteFromUriSync to provide a synchronous fallback.
→ Full coordinator example · Modular coordinator example · State restoration example
Relationship with Related Packages
| Package | Relationship |
|---|---|
zenrouter_core |
Platform-independent core: RouteTarget, CoordinatorCore, StackPath, and all route mixins |
zenrouter |
Flutter integration: Coordinator, NavigationStack, StackTransition, state restoration |
zenrouter_devtools |
DevTools extension for inspecting routes, testing deep links, and debugging navigation |
zenrouter_file_generator |
Optional build_runner code generator for Next.js-style file-based routing on top of Coordinator |
Documentation
Guides
API Reference
Recipes
- 404 Handling
- Authentication Flow
- Bottom Navigation
- Route Transitions
- State Management Integration
- URL Strategies
- Coordinator as Route Module
Migration Guides
Contributing
See CONTRIBUTING.md for guidelines.
License
Apache 2.0 — see LICENSE.