jetx_generator 0.1.0-alpha.3
jetx_generator: ^0.1.0-alpha.3 copied to clipboard
Code generator for JetX routes
JetX Route Generator #
Code generator for JetX routes using annotations. Provides type-safe navigation with automatic parameter mapping using a centralized router approach .
Table of Contents #
- Features
- Quick Start
- Setup
- Annotations
- Generated Code
- Navigation Patterns
- Advanced Features
- Best Practices
- Troubleshooting
- Migration Guide
- FAQ
- Architecture
Features #
- 🎯 Type-safe navigation - Compile-time route parameter checking
- 🚀 Centralized router - All routes defined in one place
- 📦 Parameter mapping - Automatic conversion of path and query parameters
- 🔗 Complex object support - Pass custom classes and collections as arguments
- 🛡️ Deep linking - Shareable URLs with primitive parameters
- ✨ Clean syntax: Intuitive and easy route annotations
- 🔄 Auto-detection - Complex types automatically passed as arguments
- ⚡ Performance - Zero runtime overhead, compile-time generation
Quick Start #
Code Generation Flow #
graph TD
A[Annotate Pages with @RoutePage] --> B[Create AppRouter with @JetRouter]
C[Run build_runner] --> D[Generated Route Classes]
D --> E[Type-safe Navigation]
F[Primitive Types] --> G[URL Parameters]
H[Complex Types] --> I[Arguments Map]
style A fill:#e1f5fe
style D fill:#c8e6c9
style E fill:#fff3e0
Setup #
1. Add dependencies #
dependencies:
jetx: ^0.1.0-alpha.3
jetx_annotations: ^0.1.0-alpha.3
dev_dependencies:
jetx_generator: ^0.1.0-alpha.3
build_runner: ^2.4.0
2. Annotate your pages #
// pages/user_page.dart
import 'package:flutter/material.dart';
import 'package:jetx/jetx.dart';
import 'package:jetx_annotations/jetx_annotations.dart';
@RoutePage(path: '/user/:userId')
class UserPage extends StatelessWidget {
final int userId;
@QueryParam()
final String? tab;
const UserPage({
super.key,
required this.userId,
this.tab,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User $userId')),
body: Text('Tab: $tab'),
);
}
}
3. Create app router #
// app_router.dart
import 'package:jetx/jetx.dart';
import 'package:jetx_annotations/jetx_annotations.dart';
import 'pages/home_page.dart';
import 'pages/user_page.dart';
part 'app_router.g.dart';
@JetRouter()
class AppRouter {
static List<JetPage> get pages => [
HomePageRoute.build(),
UserPageRoute.build(),
];
}
4. Run code generation #
flutter pub run build_runner build --delete-conflicting-outputs
5. Use generated routes #
// main.dart
import 'package:flutter/material.dart';
import 'package:jetx/jetx.dart';
import 'app_router.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return JetMaterialApp(
title: 'JetX Router Example',
getPages: AppRouter.pages,
);
}
}
// Type-safe navigation
const UserPageRoute(userId: 42, tab: 'posts').push();
Annotations #
@RoutePage #
Main annotation for marking a page as a route.
@RoutePage(
path: '/custom/:id', // Optional: custom path (default: auto-generated)
name: 'customRoute', // Optional: route name
transition: 'fadeIn', // Optional: transition type
transitionDurationMs: 300, // Optional: transition duration
fullscreenDialog: false, // Optional: fullscreen dialog
maintainState: true, // Optional: maintain state
preventDuplicates: true, // Optional: prevent duplicate routes
)
Path generation:
- If
pathis null, it's auto-generated from the class name UserPage→/userUserDetailPage→/user-detailHomePage→/home
Transition types:
fadeIn,fadeOut,fadeInToUp,fadeInToDownrightToLeft,leftToRight,upToDown,downToUprightToLeftWithFade,leftToRightWithFadezoomIn,zoomOut,topLevel,noTransition
@PathParam #
Mark a constructor parameter as a path parameter.
@RoutePage(path: '/product/:productId')
class ProductPage extends StatelessWidget {
@PathParam(name: 'productId')
final int productId;
const ProductPage({super.key, required this.productId});
}
@QueryParam #
Mark a constructor parameter as a query parameter.
@QueryParam(name: 'tab', defaultValue: 'home')
final String? tab;
@QueryParam()
final String? category; // Uses field name as parameter name
Supported types:
String,int,double,bool,numDateTime(ISO 8601 format)- Nullable versions of all above
@ArgumentParam #
Explicitly mark a parameter as an argument (not in URL).
@RoutePage(path: '/profile')
class ProfilePage extends StatelessWidget {
@ArgumentParam()
final String privateToken; // Forced to be argument (hidden from URL)
const ProfilePage({super.key, required this.privateToken});
}
Complex Objects (Auto-Detected as Arguments) #
New in this version: Complex types like custom classes and List<T> are automatically detected and passed as arguments instead of URL parameters.
Auto-Detection
Primitive types (int, double, bool, String, num, DateTime) are passed in the URL.
Complex types (custom classes, List, Map, etc.) are automatically passed as arguments.
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
@RoutePage(path: '/user/:userId')
class UserPage extends StatelessWidget {
final int userId; // ✅ In URL (primitive)
@QueryParam()
final String? tab; // ✅ In URL as query param (primitive)
final User user; // ✅ Auto-detected as argument (complex)
final List<String>? tags; // ✅ Auto-detected as argument (complex)
const UserPage({
super.key,
required this.userId,
this.tab,
required this.user,
this.tags,
});
}
Generated Code
The generator creates navigation methods that automatically pass complex objects as arguments:
// Navigate with both URL params and complex objects
UserPageRoute(
userId: 42, // Goes in URL: /user/42?tab=posts
tab: 'posts', // Goes in URL
user: userObject, // Passed as argument (not in URL)
tags: ['flutter', 'dart'], // Passed as argument
).push();
// Generates internally:
Jet.toNamed(
'/user/42?tab=posts',
arguments: {
'user': userObject,
'tags': ['flutter', 'dart'],
},
);
Benefits
- Type Safety: Pass full objects without serialization
- Deep Linking: Only primitive types in URL (shareable URLs)
- Flexibility: Complex data like callbacks, models, and collections
- Automatic: No extra configuration needed
@JetRouter #
Collect routes into a centralized router.
@JetRouter()
class AppRouter {
static List<JetPage> get pages => [
HomePageRoute.build(),
UserPageRoute.build(),
ProfilePageRoute.build(),
];
}
Generated Code #
The generator creates a single app_router.g.dart file containing:
1. Route Data Classes #
All generated route classes extend NavigableRoute which provides common navigation methods:
class HomePageRoute extends NavigableRoute {
const HomePageRoute();
@override
String get path => '/home';
}
class UserPageRoute extends NavigableRoute {
const UserPageRoute({
required this.userId,
this.tab,
});
final int userId;
final String? tab;
@override
String get path => '/user/$userId' + (tab != null ? '?tab=$tab' : '');
}
Note: All navigation methods are inherited from the NavigableRoute base class, keeping the generated code clean and maintainable.
Available Navigation Methods:
push<T>()- Navigate to this routepushReplacement<T>()- Replace current routepushAndRemoveUntil<T>(predicate)- Remove routes until conditionpushAndRemoveAll<T>()- Clear stack and push this routepushWithArgs<T>(arguments)- Navigate with arguments (invisible, any type)pushReplacementWithArgs<T>(arguments)- Replace with argumentspushWithParameters<T>(parameters)- Navigate with parameters (URL query string)isActive- Check if route is currently activerouteName- Get the route path
Key Difference:
- Parameters (
pushWithParameters) → visible in URL, strings only, for deep links - Arguments (
pushWithArgs) → invisible, any type, for complex objects
2. Static Methods #
Each route class includes static methods for building JetPage instances:
class UserPageRoute extends NavigableRoute {
// Static path getter
static String get routePath => '/user';
// Static page builder
static Widget Function() get page => () {
final userId = int.parse(Jet.parameters['userId']!);
final tab = Jet.parameters['tab'];
return UserPage(userId: userId, tab: tab);
};
// Static build method with all JetPage options
static JetPage build({
Transition? transition,
Duration? transitionDuration,
bool? fullscreenDialog,
bool? maintainState,
bool? preventDuplicates,
// ... all other JetPage parameters
}) {
return JetPage(
name: routePath,
page: page,
transition: transition,
transitionDuration: transitionDuration,
fullscreenDialog: fullscreenDialog ?? false,
maintainState: maintainState ?? true,
preventDuplicates: preventDuplicates ?? true,
);
}
}
3. Parameter Extraction #
The generator automatically converts route parameters based on their types:
// Generated parameter extraction
final userId = int.parse(Jet.parameters['userId']!);
final tabStr = Jet.parameters['tab'];
final tab = tabStr != null ? tabStr : null as String?;
Type conversions:
int→int.parse(param)double→double.parse(param)bool→param == 'true'String→param(no conversion)DateTime→DateTime.parse(param)
Navigation Patterns #
Basic Navigation #
// Simple navigation
const HomePageRoute().push();
// With parameters
const UserPageRoute(userId: 42, tab: 'posts').push();
Navigation Methods #
final route = UserPageRoute(userId: 42);
// Push (add to stack)
await route.push();
// Replace current route
await route.pushReplacement();
// Remove until condition
await route.pushAndRemoveUntil((page) => page.name == '/home');
// Clear all and push
await route.pushAndRemoveAll();
Passing Complex Objects #
class Product {
final int id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
}
@RoutePage(path: '/product/:productId')
class ProductPage extends StatelessWidget {
final int productId;
final Product product; // Auto-detected as argument
const ProductPage({
super.key,
required this.productId,
required this.product,
});
}
// Navigation with complex object
final product = Product(id: 1, name: 'Widget', price: 29.99);
const ProductPageRoute(productId: 1, product: product).push();
Checking Active Routes #
// Check if route is currently active
if (UserPageRoute(userId: 42).isActive) {
print('User page is active');
}
// Get current route name
final currentRoute = Jet.currentRoute;
Programmatic Navigation #
// Navigate with custom arguments
UserPageRoute(userId: 42).pushWithArgs({
'customData': someObject,
'callback': () => print('Hello'),
});
// Navigate with query parameters
UserPageRoute(userId: 42).pushWithParameters({
'tab': 'posts',
'sort': 'date',
});
Advanced Features #
Nested Routes #
Create hierarchical route structures:
@RoutePage(path: '/dashboard')
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text('Dashboard'),
Expanded(
child: Navigator(
onGenerateRoute: (settings) {
switch (settings.name) {
case '/dashboard/users':
return MaterialPageRoute(
builder: (_) => const UsersPage(),
);
case '/dashboard/settings':
return MaterialPageRoute(
builder: (_) => const SettingsPage(),
);
default:
return MaterialPageRoute(
builder: (_) => const DashboardHomePage(),
);
}
},
),
),
],
),
);
}
}
Custom Transitions #
@RoutePage(
path: '/profile',
transition: 'fadeIn',
transitionDurationMs: 500,
)
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: const Center(child: Text('Profile Page')),
);
}
}
// Or override at navigation time
ProfilePageRoute().push(
transition: Transition.fadeIn,
transitionDuration: const Duration(milliseconds: 300),
);
Deep Linking #
Handle external URLs and app links:
@RoutePage(path: '/product/:productId')
class ProductPage extends StatelessWidget {
final int productId;
@QueryParam()
final String? variant;
const ProductPage({
super.key,
required this.productId,
this.variant,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product $productId')),
body: Column(
children: [
Text('Product ID: $productId'),
if (variant != null) Text('Variant: $variant'),
],
),
);
}
}
// Deep link: https://myapp.com/product/123?variant=red
// Automatically navigates to ProductPage with productId=123, variant='red'
Route Names #
Use custom route names for better organization:
@RoutePage(
path: '/user/:userId',
name: 'userProfile',
)
class UserPage extends StatelessWidget {
final int userId;
const UserPage({super.key, required this.userId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('User $userId')),
body: const Center(child: Text('User Profile')),
);
}
}
// Use custom name for navigation
Jet.toNamed('/user/42', name: 'userProfile');
Complex Type Handling #
class Order {
final String id;
final List<OrderItem> items;
final DateTime createdAt;
Order({required this.id, required this.items, required this.createdAt});
}
class OrderItem {
final String productId;
final int quantity;
final double price;
OrderItem({required this.productId, required this.quantity, required this.price});
}
@RoutePage(path: '/order/:orderId')
class OrderPage extends StatelessWidget {
final String orderId;
final Order order; // Complex object, passed as argument
const OrderPage({
super.key,
required this.orderId,
required this.order,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Order ${order.id}')),
body: ListView.builder(
itemCount: order.items.length,
itemBuilder: (context, index) {
final item = order.items[index];
return ListTile(
title: Text('Product ${item.productId}'),
subtitle: Text('Qty: ${item.quantity}'),
trailing: Text('\$${item.price}'),
);
},
),
);
}
}
// Navigation with complex object
final order = Order(
id: 'ORD-001',
items: [
OrderItem(productId: 'P1', quantity: 2, price: 29.99),
OrderItem(productId: 'P2', quantity: 1, price: 19.99),
],
createdAt: DateTime.now(),
);
const OrderPageRoute(orderId: 'ORD-001', order: order).push();
Best Practices #
Route Organization #
1. Centralized Router
// ✅ Good: Single router file
@JetRouter()
class AppRouter {
static List<JetPage> get pages => [
HomePageRoute.build(),
UserPageRoute.build(),
ProductPageRoute.build(),
];
}
// ❌ Bad: Scattered route definitions
class HomeRouter {
static List<JetPage> get homePages => [HomePageRoute.build()];
}
class UserRouter {
static List<JetPage> get userPages => [UserPageRoute.build()];
}
2. Logical Grouping
// Group related routes
@JetRouter()
class AppRouter {
static List<JetPage> get pages => [
// Authentication
LoginPageRoute.build(),
RegisterPageRoute.build(),
// Main App
HomePageRoute.build(),
ProfilePageRoute.build(),
// Features
ProductPageRoute.build(),
CartPageRoute.build(),
CheckoutPageRoute.build(),
];
}
Parameter Strategy #
1. Use URL Parameters for Deep Linking
// ✅ Good: Shareable URLs
@RoutePage(path: '/product/:productId')
class ProductPage extends StatelessWidget {
final int productId; // In URL for deep linking
final Product product; // Complex object as argument
const ProductPage({
super.key,
required this.productId,
required this.product,
});
}
// ❌ Bad: Everything as arguments
@RoutePage(path: '/product')
class ProductPage extends StatelessWidget {
@ArgumentParam()
final int productId; // Not shareable
const ProductPage({super.key, required this.productId});
}
2. Query Parameters for Optional Data
// ✅ Good: Optional filters in URL
@RoutePage(path: '/products')
class ProductsPage extends StatelessWidget {
@QueryParam()
final String? category;
@QueryParam()
final String? sort;
const ProductsPage({
super.key,
this.category,
this.sort,
});
}
// URL: /products?category=electronics&sort=price
Performance Optimization #
1. Lazy Loading
// Use lazy loading for heavy pages
@RoutePage(path: '/heavy-page')
class HeavyPage extends StatelessWidget {
const HeavyPage({super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _loadHeavyData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
return HeavyContent(data: snapshot.data);
},
);
}
}
2. Route Caching
// Cache frequently accessed routes
class RouteCache {
static final Map<String, Widget> _cache = {};
static Widget getCachedRoute(String routeName, Widget Function() builder) {
return _cache.putIfAbsent(routeName, builder);
}
}
Code Generation Workflow #
1. Development Mode
# Watch for changes during development
flutter pub run build_runner watch
2. Production Build
# Clean build for production
flutter pub run build_runner build --delete-conflicting-outputs
3. CI/CD Integration
# .github/workflows/build.yml
- name: Generate code
run: flutter pub run build_runner build --delete-conflicting-outputs
Testing Strategies #
1. Unit Tests for Route Classes
void main() {
group('UserPageRoute', () {
test('should generate correct path', () {
const route = UserPageRoute(userId: 42, tab: 'posts');
expect(route.path, equals('/user/42?tab=posts'));
});
test('should handle null query parameters', () {
const route = UserPageRoute(userId: 42);
expect(route.path, equals('/user/42'));
});
});
}
2. Widget Tests with Generated Routes
void main() {
testWidgets('UserPage displays correct content', (tester) async {
await tester.pumpWidget(
JetMaterialApp(
getPages: AppRouter.pages,
initialRoute: '/user/42?tab=posts',
),
);
expect(find.text('User 42'), findsOneWidget);
expect(find.text('Tab: posts'), findsOneWidget);
});
}
Troubleshooting #
Common Build Errors #
1. Conflicting Outputs
Error: Conflicting outputs: lib/app_router.g.dart
Solution:
flutter pub run build_runner build --delete-conflicting-outputs
2. Missing Part Directive
Error: The part file 'app_router.g.dart' is missing
Solution:
// Add this to your router file
part 'app_router.g.dart';
3. Type Mismatch
Error: The argument type 'String' can't be assigned to the parameter type 'int'
Solution:
// ✅ Correct: Match parameter types
@RoutePage(path: '/user/:userId')
class UserPage extends StatelessWidget {
final int userId; // Must match path parameter type
const UserPage({super.key, required this.userId});
}
// ❌ Wrong: Type mismatch
@RoutePage(path: '/user/:userId')
class UserPage extends StatelessWidget {
final String userId; // Wrong type for int parameter
}
Missing Parameter Errors #
1. Required Parameters
Error: The named parameter 'userId' is required, but there's no corresponding argument
Solution:
// ✅ Provide all required parameters
const UserPageRoute(userId: 42).push();
// ❌ Missing required parameter
const UserPageRoute().push(); // Missing userId
2. Optional Parameters
// ✅ Handle optional parameters correctly
@RoutePage(path: '/search')
class SearchPage extends StatelessWidget {
@QueryParam()
final String? query; // Nullable for optional
const SearchPage({super.key, this.query});
}
Debug Tips #
1. Check Generated Code
// Look at app_router.g.dart to see what was generated
class UserPageRoute extends NavigableRoute {
const UserPageRoute({required this.userId});
final int userId;
@override
String get path => '/user/$userId';
}
2. Verify Parameter Extraction
// Check the generated page builder
static Widget Function() get page => () {
final userId = int.parse(Jet.parameters['userId']!);
return UserPage(userId: userId);
};
3. Test Navigation
// Test route generation
void testRoute() {
const route = UserPageRoute(userId: 42);
print('Generated path: ${route.path}'); // Should print: /user/42
}
Performance Issues #
1. Large Route Files
Warning: Generated file is very large (>1000 lines)
Solution:
// Split into multiple routers
@JetRouter()
class AuthRouter {
static List<JetPage> get pages => [
LoginPageRoute.build(),
RegisterPageRoute.build(),
];
}
@JetRouter()
class MainRouter {
static List<JetPage> get pages => [
HomePageRoute.build(),
ProfilePageRoute.build(),
];
}
// Combine in main app
JetMaterialApp(
getPages: [
...AuthRouter.pages,
...MainRouter.pages,
],
);
2. Slow Build Times
Warning: Code generation is taking longer than expected
Solution:
# Use watch mode for development
flutter pub run build_runner watch
# Clean build for production
flutter clean
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs
Migration Guide #
From Manual Routes #
Before (Manual):
// Manual route definitions
JetMaterialApp(
getPages: [
JetPage(
name: '/user/:userId',
page: () {
final userId = int.parse(Jet.parameters['userId']!);
final tab = Jet.parameters['tab'];
return UserPage(userId: userId, tab: tab);
},
),
],
)
// Manual navigation (not type-safe)
Jet.toNamed('/user/42?tab=posts');
After (Generated):
// 1. Annotate your page
@RoutePage(path: '/user/:userId')
class UserPage extends StatelessWidget {
final int userId;
@QueryParam()
final String? tab;
const UserPage({super.key, required this.userId, this.tab});
}
// 2. Create router
@JetRouter()
class AppRouter {
static List<JetPage> get pages => [
UserPageRoute.build(),
];
}
// 3. Use generated routes
JetMaterialApp(getPages: AppRouter.pages);
// 4. Type-safe navigation
const UserPageRoute(userId: 42, tab: 'posts').push();
// Navigation context.router.push(UserRoute(userId: 42));
**After (JetX Generator):**
```dart
@RoutePage(path: '/user/:userId')
class UserPage extends StatelessWidget {
final int userId;
const UserPage({super.key, required this.userId});
}
@JetRouter()
class AppRouter {
static List<JetPage> get pages => [
UserPageRoute.build(),
];
}
// Navigation
const UserPageRoute(userId: 42).push();
From go_router #
Before (go_router):
final router = GoRouter(
routes: [
GoRoute(
path: '/user/:userId',
builder: (context, state) {
final userId = int.parse(state.pathParameters['userId']!);
return UserPage(userId: userId);
},
),
],
);
// Navigation
context.go('/user/42');
After (JetX Generator):
@RoutePage(path: '/user/:userId')
class UserPage extends StatelessWidget {
final int userId;
const UserPage({super.key, required this.userId});
}
@JetRouter()
class AppRouter {
static List<JetPage> get pages => [
UserPageRoute.build(),
];
}
// Navigation
const UserPageRoute(userId: 42).push();
Step-by-Step Migration #
1. Add Dependencies
dependencies:
jetx: ^0.1.0-alpha.3
jetx_annotations:
path: packages/jetx_annotations
dev_dependencies:
jetx_generator:
path: packages/jetx_generator
build_runner: ^2.4.0
2. Annotate Existing Pages
// Add @RoutePage annotation
@RoutePage(path: '/existing-path')
class ExistingPage extends StatelessWidget {
// Keep existing constructor parameters
const ExistingPage({super.key, required this.someParam});
}
3. Create Router
// Create app_router.dart
@JetRouter()
class AppRouter {
static List<JetPage> get pages => [
ExistingPageRoute.build(),
// Add other routes
];
}
4. Update Main App
// Replace manual getPages with generated ones
JetMaterialApp(
getPages: AppRouter.pages, // Instead of manual list
);
5. Update Navigation
// Replace manual navigation
// Before: Jet.toNamed('/path/42')
// After: const ExistingPageRoute(someParam: 42).push()
6. Run Code Generation
flutter pub run build_runner build --delete-conflicting-outputs
7. Test and Iterate
- Test all navigation flows
- Verify deep linking works
- Check parameter passing
- Update any remaining manual navigation calls
FAQ #
General Questions #
Q: Can I use JetX Generator with other state management solutions? A: Yes, JetX Generator only handles routing and can work with any state management solution. The generated routes are just JetPage instances that can be used in any Flutter app.
Q: Is there a performance impact? A: No, there's zero runtime overhead. All code generation happens at build time, and the generated code is optimized for performance.
Q: Can I mix generated routes with manual routes? A: Yes, you can combine generated routes with manual JetPage definitions:
JetMaterialApp(
getPages: [
...AppRouter.pages, // Generated routes
JetPage( // Manual route
name: '/manual',
page: () => ManualPage(),
),
],
);
Technical Questions #
Q: How are complex objects handled in deep linking? A: Complex objects are automatically passed as arguments (not in the URL) to maintain shareable URLs. Only primitive types appear in the URL for deep linking compatibility.
Q: Can I customize the generated code? A: The generated code is read-only and should not be modified. All customization should be done through annotations and the build method parameters.
Q: What happens if I change a route annotation?
A: You need to run flutter pub run build_runner build to regenerate the code. The generator will update the route classes to match your changes.
Q: How do I handle route parameters that aren't strings? A: The generator automatically converts string parameters to the correct types:
@RoutePage(path: '/user/:userId')
class UserPage extends StatelessWidget {
final int userId; // Automatically converted from string
const UserPage({super.key, required this.userId});
}
Limitations #
Q: Are there any limitations to the generator? A: Current limitations include:
- No support for nested route structures (use manual navigation within pages)
- No support for route guards (implement in page widgets)
- No support for route transitions beyond basic JetX transitions
- Complex object serialization is not supported (objects are passed as-is)
Q: Can I use the generator with web applications? A: Yes, the generator works with all Flutter platforms including web, mobile, and desktop.
Q: How do I handle authentication with generated routes? A: Implement authentication checks in your page widgets or use a wrapper widget:
@RoutePage(path: '/protected')
class ProtectedPage extends StatelessWidget {
const ProtectedPage({super.key});
@override
Widget build(BuildContext context) {
return AuthWrapper(
child: ProtectedContent(),
);
}
}
Best Practices #
Q: How should I organize my routes? A: Use a single router class with logical grouping:
@JetRouter()
class AppRouter {
static List<JetPage> get pages => [
// Authentication
LoginPageRoute.build(),
RegisterPageRoute.build(),
// Main app
HomePageRoute.build(),
ProfilePageRoute.build(),
// Features
ProductPageRoute.build(),
CartPageRoute.build(),
];
}
Q: When should I use arguments vs query parameters? A: Use query parameters for optional, shareable data. Use arguments for complex objects or sensitive data:
@RoutePage(path: '/product/:productId')
class ProductPage extends StatelessWidget {
final int productId; // Path parameter (required, shareable)
@QueryParam()
final String? variant; // Query parameter (optional, shareable)
final Product product; // Argument (complex object, not shareable)
}
Q: How do I test generated routes? A: Test the generated route classes and navigation:
void main() {
test('UserPageRoute generates correct path', () {
const route = UserPageRoute(userId: 42, tab: 'posts');
expect(route.path, equals('/user/42?tab=posts'));
});
}
Architecture #
The JetX Generator follows a clean, modular architecture:
Components #
- jetx_annotations: Annotation classes and metadata
- jetx_generator: Code generation logic using
source_genandcode_builder - build_runner: Build system integration
- Generated code: Type-safe route classes and navigation methods
Code Generation Flow #
graph TD
A[Source Files with Annotations] --> B[Route Parser]
B --> C[Route Config Models]
C --> D[Router Parser]
D --> E[Router Config]
E --> F[Code Generator]
F --> G[Generated .g.dart Files]
H[RoutePage Annotations] --> B
I[JetRouter Annotations] --> D
style A fill:#e3f2fd
style G fill:#c8e6c9
Generated Code Structure #
// For each @RoutePage annotated class, generates:
class [ClassName]Route extends NavigableRoute {
// Constructor with parameters
const [ClassName]Route({...});
// Instance fields
final Type param;
// Static path getter
static String get routePath => '/path';
// Static page builder
static Widget Function() get page => () => [ClassName](...);
// Static build method
static JetPage build({...}) => JetPage(...);
// Instance path getter
@override
String get path => '/path/with/params';
// Navigation method overrides (if has arguments)
@override
Future<T?> push<T>() => Jet.toNamed<T>(path, arguments: {...});
}
Dependencies #
- build: Build system framework
- source_gen: Code generation utilities
- code_builder: Dart code generation
- analyzer: Dart code analysis
- dart_style: Code formatting
Integration Points #
- JetX Framework: Uses JetPage, Jet.toNamed, etc.
- Flutter: Standard Widget and navigation APIs
- Build Runner: Standard Flutter build system
License #
Same as JetX framework - MIT License
Need help? Check the troubleshooting section or open an issue on GitHub.