zenrouter_file_generator 0.2.2 copy "zenrouter_file_generator: ^0.2.2" to clipboard
zenrouter_file_generator: ^0.2.2 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 File Generator #

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.

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
  • 📦 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

Installation #

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

dependencies:
  zenrouter: ^0.2.1
  zenrouter_file_annotation: ^0.2.1

dev_dependencies:
  build_runner: ^2.10.4
  zenrouter_file_generator: ^0.2.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
└── 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')),
    );
  }
}

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: RouteLayout.buildPrimitivePath(
        IndexedStackPath, coordinator, path, this,
      ),
      // 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
_layout.dart - Layout wrapper (not a route)
_*.dart - Private files (ignored)
(group)/ - Route group (layout without URL segment)

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: RouteLayout.buildPrimitivePath(
          NavigationPath,
          coordinator,
          resolvePath(coordinator),
          this,
        ),
      ),
    );
  }
}

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());
}

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);
  }
}

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: RouteLayout.buildPrimitivePath(
        NavigationPath, coordinator, resolvePath(coordinator), this,
      ),
    );
  }
}

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: RouteLayout.buildPrimitivePath(
        IndexedStackPath, coordinator, path, this,
      ),
      // 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 #

MIT License - see LICENSE file for details.

6
likes
0
points
775
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

License

unknown (license)

Dependencies

analyzer, build, flutter, glob, source_gen, zenrouter_file_annotation

More

Packages that depend on zenrouter_file_generator