modugo 2.4.0
modugo: ^2.4.0 copied to clipboard
Roteamento modular e injeção de dependências para Flutter com GoRouter.
Modugo #
Modugo is a modular dependency and routing manager for Flutter/Dart that organizes the lifecycle of modules, dependencies, and routes. It is inspired by the modular architecture from go_router_modular.
The main difference is that Modugo provides full control and decoupling of automatic dependency injection and disposal based on navigation, with detailed logs and an extensible structure.
📦 Features #
- Per-module registration of dependencies with
singleton
,factory
, andlazySingleton
- Automatic lifecycle management triggered by route access or exit
- Support for imported modules (nested modules)
- Automatic disposal of unused dependencies
- Integration with GoRouter
- Support for
ShellRoute
andStatefulShellRoute
- Detailed and configurable logging
- Support for persistent modules that are never disposed
- 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
app_module.dart
app_widget.dart
main.dart
🟢 Getting Started #
main.dart #
void main() {
WidgetsFlutterBinding.ensureInitialized();
Modugo.configure(module: AppModule(), initialRoute: '/');
runApp(const AppWidget());
}
app_widget.dart #
class AppWidget extends StatelessWidget {
const AppWidget({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: Modugo.routerConfig,
title: 'Modugo App',
);
}
}
app_module.dart #
final class AppModule extends Module {
@override
void binds(IInjector i) {
i.addSingleton<AuthService>((_) => AuthService());
}
@override
List<IModule> get routes => [
ModuleRoute('/', module: HomeModule()),
ModuleRoute('/profile', module: ProfileModule()),
];
}
🔍 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(
'/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
💊 Dependency Injection #
Supported Types #
addSingleton<T>((i) => ...)
addLazySingleton<T>((i) => ...)
addFactory<T>((i) => ...)
Example #
final class HomeModule extends Module {
@override
void binds(IInjector i) {
i
..addSingleton<HomeController>((i) => HomeController(i.get<Repository>()))
..addLazySingleton<Repository>((_) => RepositoryImpl())
..addFactory<DateTime>((_) => DateTime.now());
}
@override
List<IModule> get routes => [
ChildRoute('/home', child: (context, state) => const HomePage()),
];
}
♻️ Persistent Modules #
By default, Modugo automatically disposes dependencies when a module is no longer active (i.e., when all its routes are exited). For cases like bottom navigation tabs, you may want to keep modules alive even when they are not visible.
To do this, override the persistent
flag:
final class HomeModule extends Module {
@override
bool get persistent => true;
@override
void binds(IInjector i) {
i.addLazySingleton<HomeController>(() => HomeController());
}
@override
List<IModule> get routes => [
ChildRoute('/', child: (_, __) => const HomePage()),
];
}
✅ Great for StatefulShellRoute
branches
🚫 Avoid for short-lived or heavy modules
⚖️ Lifecycle #
- Dependencies are automatically registered when accessing a module route.
- When all routes of that module are exited, dependencies are automatically disposed.
- Disposal respects
.dispose()
,.close()
, orStreamController.close()
. - The root
AppModule
is never disposed. - Dependencies in imported modules are shared and removed only when all consumers are disposed.
🚣 Navigation #
ChildRoute
#
ChildRoute('/home', child: (context, state) => const HomePage()),
ModuleRoute
#
ModuleRoute('/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> get routes => [
ShellModuleRoute(
builder: (context, state, child) => PageWidget(child: child),
routes: [
ChildRoute('/user', child: (_, __) => const UserPage()),
ChildRoute('/config', child: (_, __) => const ConfigPage()),
ChildRoute('/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.settings),
onPressed: () => context.go('/config'),
),
IconButton(
icon: const Icon(Icons.person),
onPressed: () => context.go('/user'),
),
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(
builder: (context, state, shell) => BottomBarWidget(shell: shell),
routes: [
ModuleRoute(path: '/', module: HomeModule()),
ModuleRoute(path: '/profile', module: ProfileModule()),
ModuleRoute(path: '/favorites', module: FavoritesModule()),
],
)
⚰️ 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 = Modugo.get<AuthService>();
return auth.isLoggedIn ? null : '/login';
}
}
2. Apply to a route #
ChildRoute(
'/profile',
child: (_, __) => const ProfilePage(),
guards: [AuthGuard()],
)
Or:
ModuleRoute(
'/admin',
module: AdminModule(),
guards: [AdminGuard()],
)
Guards are also supported inside ShellModuleRoute
and StatefulShellModuleRoute
branches:
StatefulShellModuleRoute(
builder: (_, __, shell) => shell,
routes: [
ModuleRoute('/settings', module: SettingsModule(), guards: [SettingsGuard()]),
ModuleRoute('/account', module: AccountModule()),
],
)
ℹ️ 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.
🔍 Accessing Dependencies #
final controller = Modugo.get<HomeController>();
Or via context extension:
final controller = context.read<HomeController>();
🧠 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.
🧼 Best Practices #
- Always specify explicit types for
addSingleton
,addLazySingleton
, andaddFactory
. - Divide your app into small, cohesive modules.
- Use
AppModule
only for global dependencies.
🤝 Contributions #
Pull requests, suggestions, and improvements are welcome!
⚙️ License #
MIT ©