Modular Router 🚀

Pub Version License Flutter

A powerful, modular, and type-safe routing solution for Flutter applications. Manage complex navigation structures, dependency injection, and authorization with ease.


✨ Features

  • 🏆 Modular Architecture: Organize your app into logical features (modules).
  • 🌲 Nested Routing: Create hierarchical path structures effortlessly.
  • 💉 Built-in Dependency Injection: Powered by auto_injector and dynamic_constructor.
  • 🛡️ Integrated Authorization: Control access at both module and route levels.
  • 📍 Type-Safe Navigation: Navigate using types instead of hardcoded strings.
  • 📱 Platform-Native Transitions: Automatic Material/Cupertino transitions.
  • 🎨 Custom Transitions: Full control over page transitions when needed.
  • 🔧 Stateful Integration: Seamlessly binds Controllers to StatefulWidgetBinder views.

🚀 Getting Started

1. Installation

Add modular_router to your pubspec.yaml:

dependencies:
  modular_router: ^2.0.0

2. Define your Modules and Routes

Create a module by extending Module and define its routes. Views should extend StatefulWidgetBinder and Controllers should implement DisposableController.

class UserModule extends Module {
  UserModule() : super(path: '/user');

  @override
  List<ModuleRoute> get routes => [
    ModuleRoute<ProfilePage>(
      path: '/profile',
      viewBuilder: ProfilePage.new,
      controllerBuilder: ProfileController.new,
    ),
    ModuleRoute<SettingsPage>(
      path: '/settings',
      viewBuilder: SettingsPage.new,
      controllerBuilder: SettingsController.new,
    ),
  ];
}

3. Initialize the Router

Create your router and pass its onGenerateRoute to your MaterialApp.

final router = ModularRouter(
  modules: [
    UserModule(),
    // ... other modules
  ],
);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: router.onGenerateRoute,
      initialRoute: '/user/profile',
    );
  }
}

💉 Dependency Injection

ModularRouter uses auto_injector to handle dependencies. You can register factories, lazy singletons, or singletons during initialization:

final router = ModularRouter(
  modules: [ ... ],
  factories: [
    Injector<AuthService>(AuthService.new),
  ],
  singletons: [
    Injector<ApiClient>(ApiClient.new),
  ],
);

Or by extending ModularRouter:

class MyRouter extends ModularRouter {
  MyRouter({required super.modules}) : super(
    factories: [ ... ],
  );
}

Dependencies are automatically injected into your viewBuilder and controllerBuilder based on their constructor parameters!

Required Base Classes

For DI and binding to work, follow these base class requirements:

  • Views: Must extend StatefulWidgetBinder.
  • Controllers: Must implement DisposableController (or use ListenableController for reactivity).
class MyController extends ListenableController {
  @override
  void dispose() {
    super.dispose();
  }
}

class MyPage extends StatefulWidgetBinder {
  const MyPage({required super.controller, super.key});

  @override
  State<MyPage> createState() => _MyPageState();
}

📍 Type-Safe Navigation

Forget about hardcoded path strings. Use the provided extensions on NavigatorState:

// Push to a specific view type
Navigator.of(context).pushTo<ProfilePage>();

// Push and replace
Navigator.of(context).pushToReplacement<SettingsPage>();

// Push and remove until
Navigator.of(context).pushToAndRemoveUntil<DashboardPage, LoginPage>();

// Pop all and push
Navigator.of(context).popAllAndPushTo<HomePage>();

📦 Passing Arguments

ModularRouter makes it incredibly easy to pass data directly into your controller or view constructors. The system handles DI automatically, but you can also pass custom arguments via the Navigator:

1. Positional Arguments

Just pass your arguments as a List:

// Controller constructor: SettingsController(this.username, this.userId)
Navigator.of(context).pushTo<SettingsPage>(
  arguments: ['tercyo', 123],
);

2. Named Arguments

Pass your arguments as a Map:

// Controller constructor: ProfileController({required String bio})
Navigator.of(context).pushTo<ProfilePage>(
  arguments: {'bio': 'Always building 🚀'},
);

3. Mixed Arguments

Pass a List with a Map as the last element:

// Controller constructor: UserDetailController(this.userId, {required String source})
Navigator.of(context).pushTo<UserDetailPage>(
  arguments: [123, {'source': 'profile_view'}],
);

🛡️ Authorization

You can easily protect routes or entire modules:

class AdminModule extends Module {
  AdminModule() : super(
    path: '/admin',
    allowAnonymous: false, // Requires authorization
  );

  @override
  List<ModuleRoute> get routes => [
    ModuleRoute<Dashboard>(
      path: '/',
      viewBuilder: AdminDashboard.new,
    ),
    ModuleRoute<PublicInfo>(
      path: '/info',
      viewBuilder: PublicInfoPage.new,
      allowAnonymous: true, // Override module setting
    ),
  ];
}

Initialize your router with the current auth state:

final router = MainRouter(
  modules: [...],
  enableAuthorize: true,
  authorized: userIsLoggedIn,
  unauthorizedRedirectRoute: '/login',
);

🛠️ Advanced Usage

Custom Page Transitions

ModuleRoute<PremiumPage>(
  path: '/premium',
  viewBuilder: PremiumPage.new,
  customPageTransition: <T>({settings, required view}) {
    return PageRouteBuilder<T>(
      settings: settings,
      pageBuilder: (context, animation, secondaryAnimation) => view,
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return FadeTransition(opacity: animation, child: child);
      },
    );
  },
);

🎨 Example

Check out the example project for a complete demonstration, including:

  • Module and Route definitions
  • Dependency Injection with a global Config object
  • Counter state management using ListenableController
  • Type-safe Navigation using NavigatorStateExtension

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

Libraries

modular_router