zenrouter 2.0.3 copy "zenrouter: ^2.0.3" to clipboard
zenrouter: ^2.0.3 copied to clipboard

A powerful Flutter router with deep linking, web support, type-safe routing, guards, redirects, and zero boilerplate.

ZenRouter Logo

The Ultimate Flutter Router for Every Navigation Pattern

pub package Test Codecov - zenrouter


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 #

  1. Create route classes extending RouteTarget
  2. Create a NavigationPath to hold the route stack
  3. Render the stack with NavigationStack, providing a resolver that maps each route to a StackTransition
  4. 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();

Full imperative example


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 #

  1. Define routes extending RouteTarget
  2. Use NavigationStack.declarative with a routes list derived from state
  3. When state changes, rebuild the routes list — 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'),
          ),
        ],
      ),
    );
  }
}

Full declarative example


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 #

  1. Define route classes extending RouteTarget with RouteUnique
  2. Create a Coordinator<T> subclass implementing parseRouteFromUri
  3. Declare NavigationPath and IndexedStackPath instances for nested stacks
  4. Wire up with MaterialApp.router using routerDelegate and routeInformationParser
  5. 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


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 #

Migration Guides #


Contributing #

See CONTRIBUTING.md for guidelines.

License #

Apache 2.0 — see LICENSE.

Created With Love By #

definev


The Ultimate Router for Flutter

DocumentationExamplesIssues

Happy Routing! 🧘

85
likes
160
points
4.2k
downloads
screenshot

Documentation

Documentation
API reference

Publisher

verified publisherzennn.dev

Weekly Downloads

A powerful Flutter router with deep linking, web support, type-safe routing, guards, redirects, and zero boilerplate.

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