jetx_generator 0.1.0-alpha.3 copy "jetx_generator: ^0.1.0-alpha.3" to clipboard
jetx_generator: ^0.1.0-alpha.3 copied to clipboard

Code generator for JetX routes

JetX Route Generator #

pub package License: MIT

Code generator for JetX routes using annotations. Provides type-safe navigation with automatic parameter mapping using a centralized router approach .

Table of Contents #

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 path is null, it's auto-generated from the class name
  • UserPage/user
  • UserDetailPage/user-detail
  • HomePage/home

Transition types:

  • fadeIn, fadeOut, fadeInToUp, fadeInToDown
  • rightToLeft, leftToRight, upToDown, downToUp
  • rightToLeftWithFade, leftToRightWithFade
  • zoomIn, 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, num
  • DateTime (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 route
  • pushReplacement<T>() - Replace current route
  • pushAndRemoveUntil<T>(predicate) - Remove routes until condition
  • pushAndRemoveAll<T>() - Clear stack and push this route
  • pushWithArgs<T>(arguments) - Navigate with arguments (invisible, any type)
  • pushReplacementWithArgs<T>(arguments) - Replace with arguments
  • pushWithParameters<T>(parameters) - Navigate with parameters (URL query string)
  • isActive - Check if route is currently active
  • routeName - 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:

  • intint.parse(param)
  • doubledouble.parse(param)
  • boolparam == 'true'
  • Stringparam (no conversion)
  • DateTimeDateTime.parse(param)

Basic Navigation #

// Simple navigation
const HomePageRoute().push();

// With parameters
const UserPageRoute(userId: 42, tab: 'posts').push();
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_gen and code_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.