zenrouter_file_generator 0.2.3
zenrouter_file_generator: ^0.2.3 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.dartfiles define layout wrappers for nested routes - 🔗 Dynamic routes -
[param].dartfiles 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:
AppRoutebase class (or custom name via@ZenCoordinator)AppCoordinatorclass withparseRouteFromUriimplementation- 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());
}
Navigation Methods: Push / Replace / Recover #
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
Example: Deep Link Handling
// 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
RouteLayoutmixin - 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.