zenrouter_file_generator 0.4.8 copy "zenrouter_file_generator: ^0.4.8" to clipboard
zenrouter_file_generator: ^0.4.8 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
  • πŸ“± 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.

Installation #

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

dependencies:
  zenrouter: ^0.4.5
  zenrouter_file_annotation: ^0.4.5

dev_dependencies:
  build_runner: ^2.10.4
  zenrouter_file_generator: ^0.4.5

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

The generator uses Dart's rest patterns for URL parsing:

// Generated parseRouteFromUri
AppRoute parseRouteFromUri(Uri uri) {
  return switch (uri.pathSegments) {
    ['docs', ...final slugs] => DocsRoute(slugs: slugs),
    ['docs', ...final slugs, final id] => DocsItemRoute(slugs: slugs, id: id),
    _ => NotFoundRoute(uri: uri),
  };
}

// 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(
      routerDelegate: coordinator.routerDelegate,
      routeInformationParser: coordinator.routeInformationParser,
    );
  }
}

// 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:

// Generated parseRouteFromUri
AppRoute parseRouteFromUri(Uri uri) {
  return switch (uri.pathSegments) {
    ['login'] => LoginRoute(),      // /login - wrapped by AuthLayout
    ['register'] => RegisterRoute(), // /register - wrapped by AuthLayout
    ['dashboard'] => DashboardRoute(),
    _ => NotFoundRoute(uri: uri),
  };
}

// 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]),
      ),
    );
  }
}

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 class with parseRouteFromUri implementation
  • Navigation path definitions for layouts
  • Type-safe navigation extension methods (push/replace/recover)
// routes.zen.dart (generated)
abstract class AppRoute extends RouteTarget with RouteUnique {}

class AppCoordinator extends Coordinator<AppRoute> {
  final IndexedStackPath<AppRoute> tabsPath = IndexedStackPath([...]);
  
  @override
  List<StackPath> get paths => [root, tabsPath];
  
  @override
  AppRoute parseRouteFromUri(Uri uri) {
    return switch (uri.pathSegments) {
      [] => IndexRoute(),
      ['about'] => AboutRoute(),
      ['profile', final id] => ProfileIdRoute(id: id),
      _ => NotFoundRoute(uri: uri),
    };
  }
}

// Type-safe navigation extensions
extension AppCoordinatorNav on AppCoordinator {
  // Push, Replace, Recover methods for each route
  Future<dynamic> pushAbout() => push(AboutRoute());
  void replaceAbout() => replace(AboutRoute());
  void recoverAbout() => recoverRouteFromUri(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) => recoverRouteFromUri(ProfileIdRoute(id: id).toUri());
}

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.recoverRouteFromUri(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

License #

Apache License 2.0 - see LICENSE file for details.

6
likes
140
points
519
downloads

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

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

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

More

Packages that depend on zenrouter_file_generator