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

A code generator for file-based routing in Flutter using zenrouter. Generate type-safe routes from your file/directory structure, similar to Next.js or Nuxt.js.

ZenRouter Logo

ZenRouter File Generator #

pub package Test Codecov - zenrouter

A code generator for file-based routing in Flutter using zenrouter. Generate type-safe routes from your file/directory structure, similar to Next.js, Nuxt.js or expo-router.

This package is part of the ZenRouter ecosystem and builds on the Coordinator paradigm for deep linking and web support.

Features #

  • πŸ—‚οΈ File = Route - Each file in routes/ becomes a route automatically
  • πŸ“ Nested layouts - _layout.dart files define layout wrappers for nested routes
  • πŸ”— Dynamic routes - [param].dart files create typed path parameters
  • 🌟 Catch-all routes - [...params].dart files capture multiple path segments
  • πŸ“¦ Route groups - (name)/ folders wrap routes in layouts without affecting URLs
  • 🎯 Type-safe navigation - Generated extension methods for type-safe navigation
  • 🧭 Declarative route graph - Generated manifest, conflict validation, and reverse routing
  • πŸ“± Full ZenRouter support - Deep linking, guards, redirects, transitions, and more
  • πŸš€ Zero boilerplate - Routes are generated from your file structure
  • πŸ•ΈοΈ Lazy loading - Routes can be lazy loaded using the deferredImport option in the @ZenCoordinator annotation. Improves app startup time and reduces initial bundle size.

Table of Contents #

Installation #

Add zenrouter_file_generator, zenrouter_file_annotation and zenrouter to your pubspec.yaml:

dependencies:
  zenrouter: ^3.0.0-beta.1
  zenrouter_file_annotation: ^3.0.0-beta.1

dev_dependencies:
  build_runner: ^2.10.4
  zenrouter_file_generator: ^3.0.0-beta.1

Quick Start #

1. Create your routes directory structure #

Organize your routes in lib/routes/ following these conventions:

lib/routes/
β”œβ”€β”€ index.dart            β†’ /
β”œβ”€β”€ about.dart            β†’ /about
β”œβ”€β”€ (auth)/               β†’ Route group (no URL segment)
β”‚   β”œβ”€β”€ _layout.dart      β†’ AuthLayout wrapper
β”‚   β”œβ”€β”€ login.dart        β†’ /login
β”‚   └── register.dart     β†’ /register
β”œβ”€β”€ profile/
β”‚   └── [id].dart         β†’ /profile/:id
β”œβ”€β”€ docs/
β”‚   └── [...slugs]/       β†’ Catch-all: /docs/a/b/c
β”‚       └── index.dart    β†’ /docs/any/path
└── tabs/
    β”œβ”€β”€ _layout.dart      β†’ Layout for tabs
    β”œβ”€β”€ feed/
    β”‚   β”œβ”€β”€ index.dart    β†’ /tabs/feed
    β”‚   └── [postId].dart β†’ /tabs/feed/:postId
    β”œβ”€β”€ profile.dart      β†’ /tabs/profile
    └── settings.dart     β†’ /tabs/settings

2. Define routes with @ZenRoute #

// lib/routes/about.dart
import 'package:flutter/material.dart';
import 'package:zenrouter/zenrouter.dart';
import 'package:zenrouter_file_generator/zenrouter_file_generator.dart';
import 'routes.zen.dart';

part 'about.g.dart';

@ZenRoute()
class AboutRoute extends _$AboutRoute {
  @override
  Widget build(AppCoordinator coordinator, BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('About')),
      body: const Center(child: Text('About Page')),
    );
  }
}

3. Dynamic parameters with [param].dart #

Files named with brackets create dynamic route parameters:

// lib/routes/profile/[id].dart
import 'package:flutter/material.dart';
import 'package:zenrouter_file_annotation/zenrouter_file_annotation.dart';
import 'routes.zen.dart';

part '[id].g.dart';

@ZenRoute()
class ProfileIdRoute extends _$ProfileIdRoute {
  ProfileIdRoute({required super.id});

  @override
  Widget build(AppCoordinator coordinator, BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Profile: $id')),
      body: Center(child: Text('User ID: $id')),
    );
  }
}

3.1 Catch-all parameters with [...params].dart #

Folders or files named with [...name] capture all remaining path segments as a List<String>. This is useful for:

  • Documentation pages: /docs/getting-started/installation
  • File paths: /files/folder/subfolder/file.txt
  • Arbitrary nested routing: /blog/2024/01/my-post-title
// lib/routes/docs/[...slugs]/index.dart
// Matches: /docs/any/number/of/segments
import 'package:flutter/material.dart';
import 'package:zenrouter_file_annotation/zenrouter_file_annotation.dart';
import 'routes.zen.dart';

part 'index.g.dart';

@ZenRoute()
class DocsRoute extends _$DocsRoute {
  DocsRoute({required super.slugs}); // slugs is List<String>

  @override
  Widget build(AppCoordinator coordinator, BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Docs: ${slugs.join('/')}'),
      body: Center(
        child: Column(
          children: [
            Text('Path segments: ${slugs.length}'),
            for (final slug in slugs) Text('- $slug'),
          ],
        ),
      ),
    );
  }
}

Combining with other parameters

You can have additional routes inside a catch-all folder:

lib/routes/
└── docs/
    └── [...slugs]/
        β”œβ”€β”€ index.dart      β†’ /docs/a/b/c (catch-all)
        β”œβ”€β”€ about.dart      β†’ /docs/a/b/c/about
        └── [id].dart       β†’ /docs/a/b/c/:id
// lib/routes/docs/[...slugs]/[id].dart
// Matches: /docs/any/path/user-123
@ZenRoute()
class DocsItemRoute extends _$DocsItemRoute {
  DocsItemRoute({required super.slugs, required super.id});

  @override
  Widget build(AppCoordinator coordinator, BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Item: $id')),
      body: Text('In path: ${slugs.join('/')}'),
    );
  }
}

Generated pattern matching

Catch-all segments become restParameters on the manifest match. The generated coordinator binds them through RouteBinding:

RouteBinding(
  id: 'DocsRoute',
  create: (match) => DocsRoute(slugs: match.restParameters['slugs']!),
),
RouteBinding(
  id: 'DocsItemRoute',
  create: (match) => DocsItemRoute(
    slugs: match.restParameters['slugs']!,
    id: match.pathParameters['id']!,
  ),
),

// Generated navigation methods
extension AppCoordinatorNav on AppCoordinator {
  Future<dynamic> pushDocs(List<String> slugs) => 
    push(DocsRoute(slugs: slugs));
  Future<dynamic> pushDocsItem(List<String> slugs, String id) => 
    push(DocsItemRoute(slugs: slugs, id: id));
}

Note: Only one catch-all parameter is allowed per route. Routes with static segments are prioritized over catch-all routes during matching.

4. Layouts with _layout.dart #

Layouts wrap child routes in a common UI structure:

// lib/routes/tabs/_layout.dart
import 'package:flutter/material.dart';
import 'package:zenrouter_file_annotation/zenrouter_file_annotation.dart';
import 'routes.zen.dart';

part '_layout.g.dart';

@ZenLayout(
  type: LayoutType.indexed,
  routes: [FeedRoute, ProfileRoute, SettingsRoute],
)
class TabsLayout extends _$TabsLayout {
  @override
  Widget build(AppCoordinator coordinator, BuildContext context) {
    final path = resolvePath(coordinator);
    
    return Scaffold(
      body: buildPath(coordinator),
      // You control the UI completely
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: path.activePathIndex,
        onTap: (i) => coordinator.push(path.stack[i]),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Feed'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
          BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'),
        ],
      ),
    );
  }
}

5. Run build_runner #

Generate the routing code:

dart run build_runner build

Or watch for changes:

dart run build_runner watch

6. Use in your app #

import 'package:flutter/material.dart';
import 'routes/routes.zen.dart';

final coordinator = AppCoordinator();

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: coordinator,
    );
  }
}

// Type-safe navigation with generated methods
coordinator.pushAbout();              // Push to /about
coordinator.pushProfileId('user-123'); // Push to /profile/user-123
coordinator.replaceIndex();            // Replace with home
coordinator.recoverTabProfile();       // Deep link to /tabs/profile

File Naming Conventions #

Pattern URL Description
index.dart /path Route at directory level
about.dart /path/about Named route
[id].dart /path/:id Dynamic parameter (single segment)
[...slugs]/ /path/* Catch-all parameter (multiple segments, List<String>)
_layout.dart - Layout wrapper (not a route)
_*.dart - Private files (ignored)
(group)/ - Route group (layout without URL segment)

Dot Notation #

You can also use dot notation in file names to represent directory nesting. This helps flatten your file structure while keeping deep URL paths.

parent.child.dart is equivalent to parent/child.dart.

Examples:

  • shop.products.[id].dart β†’ /shop/products/:id
  • settings.account.dart β†’ /settings/account
  • docs.[version].index.dart β†’ /docs/:version

This is especially useful for grouping related deep routes without creating many nested folders.

Route Groups (name) #

Route groups allow you to wrap routes with a layout without adding the folder name to the URL path. This is useful for:

  • Grouping related routes under a shared layout (e.g., auth flows)
  • Organizing routes without affecting URL structure
  • Applying different styling/themes to route groups

Example #

lib/routes/
β”œβ”€β”€ (auth)/                 # Route group - wraps routes without URL segment
β”‚   β”œβ”€β”€ _layout.dart        # AuthLayout - shared auth styling
β”‚   β”œβ”€β”€ login.dart          β†’ /login (NOT /(auth)/login)
β”‚   └── register.dart       β†’ /register (NOT /(auth)/register)
β”œβ”€β”€ (marketing)/
β”‚   β”œβ”€β”€ _layout.dart        # MarketingLayout
β”‚   β”œβ”€β”€ landing.dart        β†’ /landing
β”‚   └── pricing.dart        β†’ /pricing
└── dashboard/
    └── index.dart          β†’ /dashboard

Creating a Route Group Layout #

// lib/routes/(auth)/_layout.dart
import 'package:flutter/material.dart';
import 'package:zenrouter_file_annotation/zenrouter_file_annotation.dart';
import 'package:zenrouter/zenrouter.dart';

import '../routes.zen.dart';

part '_layout.g.dart';

@ZenLayout(type: LayoutType.stack)
class AuthLayout extends _$AuthLayout {
  @override
  Widget build(covariant AppCoordinator coordinator, BuildContext context) {
    return Scaffold(
      body: Container(
        // Auth-specific styling (gradient, logo, etc.)
        decoration: const BoxDecoration(
          gradient: LinearGradient(colors: [Colors.purple, Colors.blue]),
        ),
        child: buildPath(coordinator),
      ),
    );
  }
}

Routes Inside Route Groups #

// lib/routes/(auth)/login.dart
import 'package:flutter/material.dart';
import 'package:zenrouter_file_annotation/zenrouter_file_annotation.dart';

import '../routes.zen.dart';

part 'login.g.dart';

// URL: /login (not /(auth)/login)
// Layout: AuthLayout
@ZenRoute()
class LoginRoute extends _$LoginRoute {
  @override
  Widget build(covariant AppCoordinator coordinator, BuildContext context) {
    return Center(
      child: Column(
        children: [
          TextField(decoration: InputDecoration(labelText: 'Email')),
          TextField(decoration: InputDecoration(labelText: 'Password')),
          ElevatedButton(
            onPressed: () => coordinator.replaceIndex(),
            child: const Text('Sign In'),
          ),
        ],
      ),
    );
  }
}

Generated Code #

The generator correctly handles route groups. Parentheses folders do not appear in the URI; bindings still wrap those routes in the group layout:

RouteBinding(id: 'LoginRoute', create: (_) => LoginRoute()),
RouteBinding(id: 'RegisterRoute', create: (_) => RegisterRoute()),
RouteBinding(id: 'DashboardRoute', create: (_) => DashboardRoute()),

// Generated navigation methods
extension AppCoordinatorNav on AppCoordinator {
  Future<dynamic> pushLogin() => push(LoginRoute());
  Future<dynamic> pushRegister() => push(RegisterRoute());
}

Deferred Imports #

Improve your app's startup time by lazy-loading routes using deferred imports. When enabled, routes are only loaded when first navigated to, reducing initial bundle size.

Per-Route Configuration #

Enable deferred imports for individual routes:

@ZenRoute(deferredImport: true)
class HeavyRoute extends _$HeavyRoute {
  // Route implementation
}

Global Configuration #

Enable deferred imports for all routes via build.yaml:

# In your project's build.yaml (not the package's build.yaml)
targets:
  $default:
    builders:
      zenrouter_file_generator|zen_coordinator:
        options:
          deferredImport: true

Precedence Rules #

  1. Route annotation takes precedence: deferredImport: false in annotation overrides global config
  2. IndexedStack routes are always non-deferred: Routes in LayoutType.indexed cannot use deferred imports
  3. Otherwise, global config applies: Routes without explicit annotation use the global setting

Example with Global Config #

# build.yaml
targets:
  $default:
    builders:
      zenrouter_file_generator|zen_coordinator:
        options:
          deferredImport: true  # All routes deferred by default
// Most routes use deferred imports automatically
@ZenRoute()  // Uses global config (deferred)
class AboutRoute extends _$AboutRoute { }

// Explicitly disable for critical routes
@ZenRoute(deferredImport: false)  // Override global config
class HomeRoute extends _$HomeRoute { }

// IndexedStack routes are always non-deferred
@ZenLayout(
  type: LayoutType.indexed,
  routes: [Tab1Route, Tab2Route],  // Always non-deferred
)
class TabsLayout extends _$TabsLayout { }

Generated Code #

With deferred imports enabled:

// Generated imports
import 'about.dart' deferred as about;
import 'home.dart';  // Non-deferred (explicit or IndexedStack)

// Generated navigation
Future<void> pushAbout() async => push(await () async {
  await about.loadLibrary();
  return about.AboutRoute();
}());

Future<void> pushHome() => push(HomeRoute());  // No deferred loading

Performance Benchmarks #

Real-world benchmarks demonstrate significant initial bundle size reductions with deferred imports:

Metric Without Deferred With Deferred Improvement
Initial bundle 2,414 KB 2,155 KB -259 KB (-10.7%) βœ…
Total app size 2,719 KB 2,759 KB +40 KB (+1.5%)
Deferred chunks 0 24 chunks -

Key Benefits:

  • βœ… 10.7% faster initial load - Users see the app faster
  • βœ… On-demand loading - Routes load only when navigated to
  • βœ… Better caching - Unchanged routes won't re-download
  • ⚠️ Minimal overhead - Only 1.5% total size increase

Recommendation: For most applications, enabling deferred imports provides substantial initial load improvements with minimal trade-offs. The feature is especially effective for apps with many routes or large route components.

See the example's BENCHMARK_ANALYSIS.md for detailed measurements.

Route Mixins #

Enable advanced behaviors with annotation parameters:

import 'package:zenrouter/zenrouter.dart';
import 'package:zenrouter_file_annotation/zenrouter_file_annotation.dart';

@ZenRoute(
  guard: true,      // RouteGuard - control pop behavior
  redirect: true,   // RouteRedirect - conditional routing
  deepLink: DeeplinkStrategyType.custom, // Custom deep link handling
  transition: true, // RouteTransition - custom animations
  queries: ['search', 'page'], // Query parameters
)
class CheckoutRoute extends _$CheckoutRoute {
  @override
  FutureOr<bool> popGuard() async {
    return await confirmExit();
  }
  
  @override
  FutureOr<AppRoute?> redirect() async {
    if (!auth.isLoggedIn) return LoginRoute();
    return null; // null means proceed with this route
  }
  
  @override
  FutureOr<void> deeplinkHandler(AppCoordinator c, Uri uri) async {
    c.replace(HomeRoute());
    c.push(CartRoute());
    c.push(this);
  }
  
  @override
  Widget build(AppCoordinator coordinator, BuildContext context) {
    final searchTerm = query('search');
    final page = query('page');
    return CheckoutScreen(search: searchTerm, page: page);
  }
}

Route Query Parameters #

You can easily handle query parameters with reactive updates using the queries parameter in @ZenRoute.

1. Enable Query Support #

// Enable all query parameters
@ZenRoute(queries: ['*'])
class SearchRoute extends _$SearchRoute { ... }

// OR enable specific parameters
@ZenRoute(queries: ['q', 'page', 'sort'])
class SearchRoute extends _$SearchRoute { ... }

2. Access and Watch Queries #

Use selectorBuilder to rebuild only when specific query parameters change, avoiding unnecessary rebuilds.

@override
Widget build(AppCoordinator coordinator, BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Search Results')),
    body: Column(
      children: [
        // Rebuilds ONLY when 'q' query param changes
        selectorBuilder<String>(
          selector: (queries) => queries['q'] ?? '',
          builder: (context, searchTerm) {
            return Text('Searching for: $searchTerm');
          },
        ),
        // ... rest of UI
      ],
    ),
  );
}

3. Update Queries #

You can update queries without full navigation (preserving widget state where possible). The URL will be updated automatically.

// Update specific query param
updateQueries(
  coordinator, 
  queries: {...queries, 'page': '2'},
);

// Clear all queries
updateQueries(coordinator, queries: {});

Layout Types #

Stack Layout (NavigationPath) #

For push/pop navigation:

@ZenLayout(type: LayoutType.stack)
class SettingsLayout extends _$SettingsLayout {
  @override
  Widget build(AppCoordinator coordinator, BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: buildPath(coordinator),
    );
  }
}

Indexed Layout (IndexedStackPath) #

For tabs/drawers:

@ZenLayout(
  type: LayoutType.indexed,
  routes: [Tab1Route, Tab2Route, Tab3Route], // Order = index
)
class TabsLayout extends _$TabsLayout {
  @override
  Widget build(AppCoordinator coordinator, BuildContext context) {
    final path = resolvePath(coordinator);
    
    return Scaffold(
      body: buildPath(coordinator),
      // Full control over navigation UI
      bottomNavigationBar: YourNavigationWidget(
        index: path.activePathIndex,
        onTap: (i) => coordinator.push(path.stack[i]),
      ),
    );
  }
}

Branched Layout (BranchedStackPath) #

For stateful shells where each branch is a child layout with its own stack:

@ZenLayout(
  type: LayoutType.branched,
  branches: [HomeLayout, SearchLayout, SettingsLayout],
)
class AppShellLayout extends _$AppShellLayout {
  @override
  Widget build(AppCoordinator coordinator, BuildContext context) {
    final path = resolvePath(coordinator);

    return Scaffold(
      body: buildPath(coordinator),
      bottomNavigationBar: NavigationBar(
        selectedIndex: path.activeBranchIndex,
        onDestinationSelected: path.goToBranch,
        destinations: const [/* ... */],
      ),
    );
  }
}

Every entry in branches must be a direct child @ZenLayout. The generator creates a BranchedStackPath for the shell and a separate path for each branch, so switching branches preserves each branch's navigation depth.

Generated Code Structure #

After running build_runner, your routes directory will look like:

lib/routes/
β”œβ”€β”€ index.dart          # Your route class
β”œβ”€β”€ index.g.dart        # Generated base class
β”œβ”€β”€ about.dart
β”œβ”€β”€ about.g.dart
└── routes.zen.dart     # Generated coordinator

Generated Coordinator #

The generator creates routes.zen.dart with:

  • AppRoute base class (or custom name via @ZenCoordinator)
  • AppCoordinator.manifest, an immutable RouteManifest
  • RouteModuleBinding and generated RouteBinding / RouteBinding.deferred adapters
  • Navigation path definitions for layouts
  • Static, type-safe AppCoordinator.location.{route} reverse-routing helpers
  • Type-safe navigation extension methods (push/replace/recover)
// routes.zen.dart (generated)
abstract class AppRoute extends RouteTarget with RouteUnique {}

class AppCoordinator extends Coordinator<AppRoute>
    with RouteModuleBinding<AppRoute, String> {
  static final RouteManifest<String> manifest = RouteManifest<String>(
    name: 'AppCoordinator',
    routes: [
      RouteManifestRoute(id: 'IndexRoute', path: '/'),
      RouteManifestRoute(id: 'AboutRoute', path: '/about'),
      RouteManifestRoute(id: 'ProfileIdRoute', path: '/profile/:id'),
    ],
  );

  @override
  late final routeBindings = manifest.bind<AppRoute>(
    bindings: [
      RouteBinding(id: 'IndexRoute', create: (_) => IndexRoute()),
      RouteBinding(id: 'AboutRoute', create: (_) => AboutRoute()),
      RouteBinding(
        id: 'ProfileIdRoute',
        create: (match) => ProfileIdRoute(
          id: match.pathParameters['id']!,
        ),
      ),
    ],
    notFound: (uri) => NotFoundRoute(uri: uri),
  );

  final IndexedStackPath<AppRoute> tabsPath = IndexedStackPath([...]);
  
  @override
  List<StackPath> get paths => [root, tabsPath];

  static const location = AppCoordinatorLocation();
}

final class AppCoordinatorLocation {
  const AppCoordinatorLocation();

  Uri get about => AppCoordinator.manifest.location('AboutRoute');

  Uri profileId({required String id}) => AppCoordinator.manifest.location(
    'ProfileIdRoute',
    pathParameters: {'id': id},
  );
}

// Type-safe navigation extensions
extension AppCoordinatorNav on AppCoordinator {
  AppCoordinatorLocation get location => AppCoordinator.location;

  // Push, Replace, Recover methods for each route
  Future<dynamic> pushAbout() => push(AboutRoute());
  void replaceAbout() => replace(AboutRoute());
  void recoverAbout() => recoverUri(AboutRoute().toUri());
  
  // Routes with parameters
  Future<dynamic> pushProfileId(String id) => push(ProfileIdRoute(id: id));
  void replaceProfileId(String id) => replace(ProfileIdRoute(id: id));
  void recoverProfileId(String id) => recoverUri(ProfileIdRoute(id: id).toUri());
}

The same path pattern drives parsing and link generation:

final uri = coordinator.location.profileId(id: 'core team');
// /profile/core%20team

The generator rejects duplicate or equally-specific ambiguous patterns before emitting the coordinator. Widget builders, transitions, and route constructors remain in generated Flutter bindings rather than entering the manifest.

For each route, the generator creates three type-safe navigation methods:

Method Return Type Description
push{Route}() Future<dynamic> Push route onto stack. Returns result when popped.
replace{Route}() void Replace current route. No navigation history.
recover{Route}() void Restore full navigation state from URI. For deep links.

When to Use Each Method

push - Standard Navigation

// Navigate forward, user can go back
coordinator.pushAbout();
coordinator.pushProfileId('user-123');

// Wait for result when route pops
final result = await coordinator.pushCheckout();
if (result == 'success') { /* ... */ }

replace - Replace Current Route

// After login, replace login screen with home (no back button to login)
coordinator.replaceIndex();

// Switch tabs without adding to history
coordinator.replaceTabProfile();

recover - Deep Link / State Restoration

// Restore complete navigation state from a URI
// This rebuilds the entire navigation stack to reach the target route
coordinator.recoverProfileId('user-123');
// Equivalent to: coordinator.recoverUri(Uri.parse('/profile/user-123'));

// Use for:
// - Deep links from external sources
// - App state restoration
// - Sharing URLs that should restore full navigation context

Example: Auth Flow

// On app start - check auth and recover appropriate state
if (isLoggedIn) {
  coordinator.recoverIndex();  // Restore to home with full stack
} else {
  coordinator.replaceLogin();  // Show login, no back navigation
}

// After successful login
coordinator.replaceIndex();  // Replace login with home

// User taps profile
coordinator.pushProfileId('current-user');  // Can go back to home
// When app receives deep link: myapp://profile/user-123
void handleDeepLink(Uri uri) {
  // recover rebuilds navigation stack: [Home] -> [Profile]
  coordinator.recoverProfileId('user-123');
}

Custom Coordinator Configuration #

Customize the generated coordinator by creating lib/routes/_coordinator.dart:

// lib/routes/_coordinator.dart
import 'package:zenrouter_file_annotation/zenrouter_file_annotation.dart';

@ZenCoordinator(
  name: 'MyAppCoordinator',
  routeBase: 'MyAppRoute',
)
class CoordinatorConfig {}

Integration with ZenRouter #

This package generates routes compatible with zenrouter's coordinator pattern:

  • Routes extend RouteTarget with RouteUnique
  • Layouts use RouteLayout mixin
  • Dynamic routes have typed parameters
  • Full deep linking and URL synchronization support
  • Route guards, redirects, and transitions

See the zenrouter documentation for more details on advanced features.

Example #

Check out the /example directory for a complete working example:

cd example
flutter pub get
dart run build_runner build
flutter run

Customizing the Coordinator #

You can extend the generated coordinator to add custom capabilities, such as the debugging tools provided by zenrouter_devtools.

Usage with zenrouter_devtools #

To use the CoordinatorDebug mixin from zenrouter_devtools with your generated coordinator:

  1. Add zenrouter_devtools to your dependencies.
  2. Create a new class that extends the generated AppCoordinator.
  3. Mixin CoordinatorDebug.
// lib/routes/_coordinator_debug.dart
import 'package:flutter/foundation.dart';
import 'package:zenrouter_devtools/zenrouter_devtools.dart';
import 'routes.zen.dart';

class DebugAppCoordinator extends AppCoordinator with CoordinatorDebug<AppRoute> {
  @override
  bool get debugEnabled => kDebugMode; // Only enable in debug mode

  @override
  List<AppRoute> get debugRoutes => [
    // Pre-populate specific routes for quick testing
    const LoginRoute(),
    ProfileIdRoute(id: 'test-user'),
    const SettingsRoute(),
  ];

  @override
  String debugLabel(StackPath path) {
    // Optional: Customize how paths appear in the debugger
    if (path == root) return 'Root Stack';
    return super.debugLabel(path);
  }
}

Then use DebugAppCoordinator in your app entry point:

void main() {
  final coordinator = DebugAppCoordinator();
  
  runApp(MaterialApp.router(
    routerConfig: coordinator,
  ));
}

Why isn't this built-in? #

We chose not to generate this boilerplate automatically for several reasons:

  1. Custom Data Requirements: debugRoutes requires specific instantiation of your routes (e.g., providing dummy IDs), which the generator cannot guess using const constructors alone.
  2. Environment Logic: You may want complex logic for debugEnabled (e.g., checking flavors or environment variables) that exceeds simple boolean flags.
  3. Dependency Management: Keeping zenrouter_devtools optional ensures your production app stays lightweight if you choose not to use the devtools.

License #

Apache License 2.0 - see LICENSE file for details.

8
likes
140
points
163
downloads

Documentation

API reference

Publisher

verified publisherzennn.dev

Weekly Downloads

A code generator for file-based routing in Flutter using zenrouter. Generate type-safe routes from your file/directory structure, similar to Next.js or Nuxt.js.

Repository (GitHub)
View/report issues

License

Apache-2.0 (license)

Dependencies

analyzer, build, dart_style, flutter, glob, source_gen, zenrouter_core, zenrouter_file_annotation

More

Packages that depend on zenrouter_file_generator