modugo 3.0.0
modugo: ^3.0.0 copied to clipboard
Modular Routing and Dependency Injection for Flutter with GetIt and GoRouter.
Modugo #
Modugo is a modular system for Flutter inspired by Flutter Modular and Go Router Modular. It provides a clean structure to organize modules, routes, and dependency injection. It provides a clean way to structure your app into isolated modules, but it does not manage dependency disposal.
Key Points #
- Uses GoRouter for navigation between routes.
- Uses GetIt for dependency injection.
- Dependencies are registered once at app startup when modules are initialized.
- There is no automatic disposal of dependencies; once injected, they live for the lifetime of the application.
- Designed to provide decoupled, modular architecture without enforcing lifecycle management.
- Focuses on clarity and structure rather than automatic cleanup.
⚠️ Note: Unlike some modular frameworks, Modugo does not automatically dispose dependencies when routes are removed. All dependencies live until the app is terminated.
📦 Features #
- Integration with GoRouter
- Registration of dependencies with GetIt
- Support for imported modules (nested modules)
- Support for
ShellRoute
andStatefulShellRoute
- Detailed and configurable logging
- Built-in support for Route Guards
- Built-in support for Regex-based Route Matching
🚀 Installation #
dependencies:
modugo: x.x.x
🔹 Example Project Structure #
/lib
/modules
/home
home_page.dart
home_module.dart
/profile
profile_page.dart
profile_module.dart
/chat
chat_page.dart
chat_module.dart
app_module.dart
app_widget.dart
main.dart
🟢 Getting Started #
main.dart #
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Modugo.configure(module: AppModule(), initialRoute: '/');
runApp(
ModugoLoaderWidget(
loading: const LoadWidget(), // Your loading widget
builder: (_) => const AppWidget(),
dependencies: [ /* List of asynchronous dependencies */ ],
),
);
}
app_widget.dart #
class AppWidget extends StatelessWidget {
const AppWidget({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Modugo App',
routerConfig: Modugo.routerConfig,
);
}
}
app_module.dart #
final class AppModule extends Module {
@override
void binds() {
i.registerSingleton<AuthService>((_) => AuthService());
}
@override
List<IModule> routes() => [
ModuleRoute(path: '/', module: HomeModule()),
ModuleRoute(path: '/chat', module: ChatModule()),
ModuleRoute(path: '/profile', module: ProfileModule()),
];
}
🧠 Logging and Diagnostics #
Modugo.configure(
module: AppModule(),
debugLogDiagnostics: true,
);
- All logs pass through the
Logger
class, which can be extended or customized. - Logs include injection, disposal, navigation, and errors.
🚣 Navigation #
ChildRoute
#
ChildRoute(path: '/home', child: (context, state) => const HomePage()),
ModuleRoute
#
ModuleRoute(path: '/profile', module: ProfileModule()),
ShellModuleRoute
#
Use ShellModuleRoute
when you want to create a navigation window inside a specific area of your UI, similar to RouteOutlet
in Flutter Modular. This is commonly used in layout scenarios with menus or tabs, where only part of the screen changes based on navigation.
ℹ️ Internally, it uses GoRouter’s
ShellRoute
.
Learn more: ShellRoute docs
Module Setup
final class HomeModule extends Module {
@override
List<IModule> routes() => [
ShellModuleRoute(
builder: (context, state, child) => PageWidget(child: child),
routes: [
ChildRoute(path: '/user', child: (_, _) => const UserPage()),
ChildRoute(path: '/config', child: (_, _) => const ConfigPage()),
ChildRoute(path: '/orders', child: (_, _) => const OrdersPage()),
],
),
];
}
Shell Page
class PageWidget extends StatelessWidget {
final Widget child;
const PageWidget({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(child: child),
Row(
children: [
IconButton(
icon: const Icon(Icons.person),
onPressed: () => context.go('/user'),
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => context.go('/config'),
),
IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () => context.go('/orders'),
),
],
),
],
),
);
}
}
✅ Great for creating sub-navigation inside pages
🎯 Useful for dashboards, admin panels, or multi-section UIs
StatefulShellModuleRoute
#
StatefulShellModuleRoute is ideal for creating tab-based navigation with state preservation per tab — such as apps using BottomNavigationBar, TabBar, or any layout with parallel sections.
✅ Benefits
- Each tab has its own navigation stack.
- Switching tabs preserves their state and history.
- Seamless integration with Modugo modules, including guards and lifecycle.
🎯 Use Cases
- Bottom navigation with independent tabs (e.g. Home, Profile, Favorites)
- Admin panels or dashboards with persistent navigation
- Apps like Instagram, Twitter, or banking apps with separate stacked flows
💡 How it Works
Internally uses go_router's StatefulShellRoute to manage multiple Navigator branches. Each ModuleRoute below becomes an independent branch with its own routing stack.
StatefulShellModuleRoute(
builder: (context, state, shell) => BottomBarWidget(shell: shell),
routes: [
ModuleRoute(path: '/', module: HomeModule()),
ModuleRoute(path: '/profile', module: ProfileModule()),
ModuleRoute(path: '/favorites', module: FavoritesModule()),
],
)
🔍 Route Matching with Regex #
Modugo supports a powerful matching system using regex-based patterns. This allows you to:
- Validate paths and deep links before navigating
- Extract dynamic parameters independently of GoRouter
- Handle external URLs, web support, and custom redirect logic
Defining a pattern: #
ChildRoute(
path: '/user/:id',
routePattern: RoutePatternModel.from(r'^/user/(\d+)\$', paramNames: ['id']),
child: (_, _) => const UserPage(),
)
Matching a location: #
final match = Modugo.matchRoute('/user/42');
if (match != null) {
print(match.route); // matched route instance
print(match.params); // { 'id': '42' }
} else {
print('No match');
}
Supported Route Types: #
ChildRoute
ModuleRoute
ShellModuleRoute
StatefulShellModuleRoute
Useful for:
- Deep link validation
- Analytics and logging
- Fallback routing and redirects
🔄 Route Change Tracking #
Modugo offers a built-in mechanism to track route changes globally via a RouteNotifier
.
This is especially useful when you want to:
- Refresh parts of the UI when the location changes
- React to tab switches or deep links
- Trigger side effects like analytics or data reloading
How it works #
Modugo exposes a global RouteNotifier
instance:
Modugo.routeNotifier // type: ValueNotifier<String>
This object emits a [String] path whenever navigation occurs. You can subscribe to it from anywhere:
Modugo.routeNotifier.addListener(() {
final location = Modugo.routeNotifier.value;
if (location == '/home') {
// Action...
}
});
Example Use Case #
If your app uses dynamic tabs, webviews, or needs to react to specific navigation changes, you can use the notifier to refresh content or trigger logic based on the current or previous route.
This is especially useful in cases like:
- Restoring scroll position
- Refreshing carousels
- Triggering custom analytics
- Resetting view state
⚰️ Route Guards #
You can protect routes using IGuard
, which allows you to define redirection logic before a route is activated.
1. Define a guard #
class AuthGuard implements IGuard {
@override
FutureOr<String?> call(BuildContext context, GoRouterState state) async {
final auth = context.read<AuthService>();
return auth.isLoggedIn ? null : '/login';
}
}
2. Apply to a single route #
ChildRoute(
path: '/profile',
guards: [AuthGuard()],
child: (_, _) => const ProfilePage(),
);
3. Propagate guards to nested routes #
If you want a guard applied at a parent module level to automatically protect all child routes (even inside nested ModuleRoute
s), you can use propagateGuards
.
This is especially useful when you want consistent access control without having to manually add guards to each child route.
List<IModule> routes() => propagateGuards(
guards: [AuthGuard()],
routes: [
ModuleRoute(
path: '/',
module: HomeModule(),
),
]
);
In the example above, AuthGuard
will be automatically applied to all routes inside HomeModule
, including nested ChildRoute
s and ModuleRoute
s, without needing to repeat it manually.
ℹ️ Behavior #
- If a guard returns a non-null path, navigation is redirected.
- Guards run before the route's
redirect
logic. - Redirects are executed in order: guards ➔ route.redirect ➔ child.redirect (if ModuleRoute)
- Modugo never assumes where to redirect. It's up to you.
Dependency Injection in Modugo #
In Modugo, dependencies are registered using the binds()
method inside a Module
. You have access to i
, which is a shorthand for GetIt.instance
. You can register singletons, lazy singletons, or factories in a fluent API style similar to GetIt.
Example #
class HomeModule extends Module {
@override
List<Module> imports() => [CoreModule()];
@override
List<IModule> routes() => [
ChildRoute(path: '/', child: (context, state) => const HomePage()),
];
@override
void binds() {
i
..registerSingleton<ServiceRepository>(ServiceRepository.instance)
..registerLazySingleton<OtherServiceRepository>(OtherServiceRepositoryImpl.new);
}
}
All dependencies are registered at startup and remain alive for the full app lifecycle. They are never automatically disposed.
Notes #
registerSingleton<T>(...)
registers a singleton instance immediately.registerLazySingleton<T>(...)
registers a singleton lazily, creating it only on first access.- All registered dependencies are globally accessible via
i.get<T>()
or using Modugo’sBuildContext
extensioncontext.read<T>()
.
🤝 Contributions #
Pull requests, suggestions, and improvements are welcome!
⚙️ License #
MIT ©