go_router_modular 5.6.0
go_router_modular: ^5.6.0 copied to clipboard
Modular Routing and Dependency Injection for Flutter with GoRouter. Features event-driven architecture for seamless module communication and micro-frontend support.
5.6.0 #
- Fixed (web): browser back/forward now behave like the platform back button. Plain
go_routercreates one browser history entry per URL change, so aftercontext.go('/b')the browser back button reopened/a— a page thatgohad removed from the stack and whose module had already been disposed. The router now keeps the browser history mirrored on the page stack:goreplaces the current entry,push/nestedgoadd an entry (back pops),popmoves the browser back one entry (nothing is re-parsed, guards do not re-run), and agoaway from a deep stack collapses the history. Pressing forward onto a page that was already removed navigates to its URL as a freshgo, so guards, redirects and module binds run again (plaingo_routerrestoredpushed pages without running their redirects — the page came back without its module binds). No configuration needed;Router.neglectis not required. - Fixed: module reference counting is balanced. A module route's
redirectruns on every navigation that goes through it (sibling routes, restore afterpop, guards that redirect away), but only creates a page when the route enters the stack. Each redirect now opens at most one pending reference that the page claims when it is created; references nobody claims expire when the navigation settles. Fixes modules that were never disposed after navigating between sibling routes of the same module, afterpush/pop, or after a guard redirected away before the page was built. ThedidChangeGoingReferenceskip in module disposal was removed (reference counting already protectsA → B → Atransitions);Module.didChangeGoingReferenceandModule.onDidChangeGoingReferenceare deprecated and no longer called by the router. AStatefulShellModularRouteno longer unregisters its branch modules when the shell is disposed, because each branch page already releases its own reference; a module instance shared by a branch and another route is no longer disposed while that other page is still on screen. - Added: telemetry.
Modular.configure(onTelemetry: ...)observes the modular runtime — every change in a module's bind lifecycle (injected,disposed, and the reference kinds, each with what triggered it, the module instance id and its reference count) and every message on the event bus (eventFired,eventReceived). Events carry atoMap()for a Sentry/Crashlytics breadcrumb. The listener runs in release too and an exception it throws is swallowed, so telemetry never breaks a navigation.telemetryFilternarrows what reaches it —ModularTelemetryFilter.injections(),.modules(),.events(), or a predicate per module/event name — and is applied before the event is built, so what is filtered out costs nothing. The payload of a bus event is deliberately not carried: only its type name, because events routinely hold personal data. - Added:
Modular.resetForTesting()(@visibleForTesting) disposes the router and the injection container so the nextModular.configurebuilds everything from scratch.
5.5.2 #
- Documentation: reworked the
READMEfor a cleaner, pub.dev-friendly layout — added an Installation section and a step-by-step Quick Start (configure, compose modules, define a feature module, navigate), a features table, and a collapsible events add-on section. The layout no longer relies on centering, which pub.dev strips. - Fixed: the
go-router-modularAgent Skill was silently skipped by theskillsCLI because a:inside itsSKILL.mdfrontmatterdescriptionproduced invalid YAML. The description was corrected so the skill is discovered and installable again.
5.5.0 #
-
Route-state reads consolidated on the
Modularfacade with better names. The read utilities moved off theBuildContextextension and onto the facade, following the...Of(context)convention. Beyond path and parameters, there is now explicit access to the usefulgo_routerutilities (query params, uri, location, and typedextra) from a single entry point:final state = Modular.routerStateOf(context); // the GoRouterState final path = Modular.currentPathOf(context); // the current path final id = Modular.pathParamOf(context, 'id'); // a path parameter final params = Modular.pathParamsOf(context); // all path parameters final ref = Modular.queryParamOf(context, 'ref'); // a query parameter final queries = Modular.queryParamsOf(context); // all query parameters final uri = Modular.currentUriOf(context); // the current Uri final location = Modular.currentLocationOf(context); // the current location final payload = Modular.extraOf<MyPayload>(context); // the typed extraThe
go_routerutilities (GoRouterState,context.go,context.push, …) are still re-exported by the package barrel, so you can use them directly by importing onlypackage:go_router_modular/go_router_modular.dart.Deprecations (still functional, will be removed in a future major release):
- Extension:
context.getPathParam('id')→Modular.pathParamOf(context, 'id');context.getPath→Modular.currentPathOf(context);context.state→Modular.routerStateOf(context). - Facade:
Modular.getCurrentPathOf(context)→currentPathOf(context);Modular.stateOf(context)→routerStateOf(context).
- Extension:
5.4.0 #
-
Simplified
EventModulecomposition; unused event APIs removed. The composition mechanism based oneventImports()+ModularEventListenerwas removed (it was dead code, with no consumers). To compose another module's listeners, callAnotherEventModule().listen()synchronously inside your ownlisten()— the child's listeners inherit the host's lifecycle (they are disposed together with it, with no duplication when recreated).class EventModuleA extends EventModule { @override void listen() { on<EventFromA>((event, context) { /* ... */ }); EventModuleB().listen(); // direct composition } }In addition, the listening logic is no longer exposed as the public
EventListenerMixinmixin — it was incorporated directly intoEventModule(which is still aModule). The only public mixin in the events subsystem is nowModularEventMixin(forState<StatefulWidget>).Migration:
- Remove
eventImports()overrides and any use ofModularEventListener; move theon<T>calls into thelisten()of anEventModuleand compose viaAnotherEventModule().listen(). - If you referenced
EventListenerMixindirectly, useEventModuleinstead.
- Remove
5.3.0 #
Added #
-
copyWithon the modular router. Override individualGoRouteroptions at the point where the router is consumed, reusing everything passed toModular.configure(...). Useful whenModularApp.routerdoesn't expose an option you need — for example attaching aNavigatorObserver:MaterialApp.router( routerConfig: Modular.routerConfig.copyWith( observers: [MyAnalyticsObserver()], ), builder: (context, child) => ModularLoader.builder(context, child), );Overridable options include
observers,redirect,refreshListenable,errorBuilder,errorPageBuilder,redirectLimit,navigatorKey,restorationScopeId, and more. The derived router is memoized, so widget rebuilds reuse the same instance and navigation state is preserved.
Changed #
- Bumped the
go_routerconstraint to^17.3.0, pulling in the chained top-level redirect resolution fix (17.2.1) and thepop()+onExitstale-configuration fix (17.2.2).
5.2.0 #
Breaking #
-
Untyped factory binds no longer auto-resolve through supertypes. Applies to any supertype relationship — interfaces, abstract classes, concrete superclasses, and mixins — not just interfaces. The previous compatibility search probed every factory bind in
bindsMapto type-check it against the requested supertype, invoking the factory's constructor (with all its side effects: event publication, stream subscription, HTTP calls, etc.) and discarding the resulting instance. Production traces showed cubits being silently instantiated multiple times per session as a byproduct of unrelatedget<T>()calls. The compatibility search now skips factory binds entirely; only singletons (whose probe instance is cached and reused) participate. Migration:// Before — works by accident, instantiates ServiceImpl as a probe side-effect i.addFactory((i) => ServiceImpl()); i.get<IService>(); // ✅ resolved // After — explicit, no probe, zero side effects until first real get i.addFactory<IService>((i) => ServiceImpl()); i.get<IService>(); // ✅ resolved via Strategy 2 (direct lookup) // Or, if singleton semantics are acceptable: i.addSingleton((i) => ServiceImpl()); i.get<IService>(); // ✅ resolved (singleton cached, probe-safe)Direct lookups by the registered concrete type (
get<ServiceImpl>()) still work for untyped factories — only the supertype auto-discovery is removed.
Fixed #
- Phantom factory instances during interface lookup (the breaking change above is the fix). Reproduction: with 4 factory binds in a module, every unrelated
get<IUnregistered>instantiated all 4 (running their constructors). After 100 lookups in a session, ~400 phantom Cubit/Service instances leaked — each potentially opening WebSockets, subscribing to streams, or firing events. Now: zero phantom invocations. - Cross-type circular dependency now surfaces a clear error.
A → B → Apreviously fell into the globalhasBlockedBindsbypass and was masked asBind not found for type "A"at the deepest probe level. The validation bypass is now tightened to the specific self-reference case (addFactory<I>((i) => i.get())) using a typed(bind, requestedType)invocation stack. Cross-type cycles now throwModularException: Circular dependency detected while resolving type "A". Dependency chain: A -> B -> A. BindRegistry.registerindexes typed binds under the declared type.Bind.factory<IService>((i) => ServiceImpl())previously stored the bind only underServiceImpl(the discovered runtime type), makingget<IService>miss Strategy 2 and depend on probing. Typed binds now occupybindsMap[IService]directly; the discovered runtime type is also indexed forget<ServiceImpl>lookups.
Improved #
BindSearchProtectionis re-entrant safe. ReplacedSet<Object> _blockedBindswith a counter-backedMap<Object, int>so nested invocations of the same bind don't prematurely "unblock" each other on the firstpop.- Single helper for factory invocations. Extracted
BindLocator._withInvocation(bind, requestedType, action)— every factory call goes through here, eliminating the previous four-way duplication ofpushInvocation / try / finally / popInvocation.
5.1.0 #
Added #
- Stateful shell branch transitions:
StatefulShellBranchTransitionshelpers (e.g.withGoTransition, fade presets) so bottom tabs/branches can reuse the same transition style as modularGoRoutes.
Improved #
- Dependency injection — batch registration: Typed binds (
Bind<T>) are now indexed up front inregisterBatch, so any bind in the same batch can resolve siblings in any declaration order.commitBatchruns in three phases: materialize singletons, propagate cached instances to duplicateBindobjects, then fall back to deferred resolution forBind<Object>registrations. - Dependency injection — code quality: Extracted
_writeToCanonicalSlotto centralise the dual-map invariant (bindsMapfor unkeyed,bindsMapByKeyfor keyed binds). Replaced the ambiguousbool _handleExistingBindwith a_SlotConflictResolutionenum. Renamed_pendingBatchto_uncommittedBatch. Swallowed registration errors now surface viadart:developer.logwhendebugLogModularis enabled.
Fixed #
- Singleton identity across interface lookups:
BindLocator._searchCompatibleBindnow reuses the canonical bind when resolving through an interface, preventing a new factory call on everyInjector.get<IFoo>()call. - Self-referential interface factory (
addFactory<I>((i) => i.get())): The locator now detects recursive resolution for the same type and skips the executing bind, falling through to the concrete implementation. Previously this threw"Type IFoo is already being searched". ConcurrentModificationErrorduring interface lookup:_searchCompatibleBindnow iterates a snapshot ofbindsMap.entriesinstead of the live view, making in-loop writes safe under nested resolution.- Singleton instantiated multiple times via imports: Duplicate
Bindobjects created by re-imported modules now receive the cached instance from the already-registered bind, preventing repeated factory calls. Injector.get<IService>()after typed registration: A singleton registered with an explicit generic (e.g.i.addSingleton<IAuthApi>(...)) is now reachable through both the interface and the concreteruntimeType.- Keyed + unkeyed singleton on the same type:
bindsMap[type]now holds only the unkeyed bind; keyed binds live exclusively inbindsMapByKey, makingInjector.get<IClient>()order-independent.
5.0.6 #
Fixed #
- Duplicate singleton construction via imports:
_collectImportedBindscreated newBindobjects withcachedInstance == nullon every registration pass.commitBatchnow propagatescachedInstanceto duplicate binds before downstream methods run, preventing 2× extra factory calls. - Orphaned singleton instances: When two imported modules declare the same type,
commitBatchnow checks_isSingletonAlreadyRegisteredbefore callingfactoryFunction, completely preventing duplicate factory calls and leaked instances (open streams, duplicate subscriptions).
5.0.5 #
Fixed #
- Dependency injection: Singleton and lazySingleton constructors were called multiple times during module registration. The system now correctly reuses the cached instance after the first creation.
5.0.4 #
Fixed #
onExitinChildRoute: Fixed propagation of theonExitcallback across all route creation paths.
5.0.3 #
Added #
- Detailed error messages: Missing dependency errors now include which component made the request and a full dependency chain (e.g.
A ➔ B ➔ C). - Circular dependency detection: Detects infinite recursion during bind resolution and surfaces a clear, actionable error message.
- Safer resource cleanup:
CleanBindnow handlesdispose,close, andcancelmore robustly, with safe fallback forNoSuchMethodError.
5.0.2 #
Improved #
- Dependency injection: Switched from nested search to a commit-based approach for better performance.
5.0.1 #
Added #
-
Bind.lazySingleton: Creates singleton instances only on first access — useful for expensive resources that may not always be needed.i.lazySingleton<ExpensiveService>((i) => ExpensiveService()); -
Page transitions: Built-in transition system for smooth route animations.
- Presets: fade, slide, scale, rotate, and more.
- Child routes automatically inherit transitions from parent modules.
- Platform-specific styles: Cupertino (iOS/macOS) and Material (Android).
- Chainable effects:
GoTransitions.slide.toRight.withFade.
ModuleRoute('/home', module: HomeModule(), transition: GoTransitions.fadeUpwards, duration: Duration(milliseconds: 300)) ChildRoute('/details', child: (_, __) => DetailsPage(), transition: GoTransitions.slide.toRight.withFade)
5.0.0 #
Breaking Changes #
-
New
bindsAPI: Changed fromFutureOr<List<Bind<Object>>> binds()toFutureBinds binds(Injector i). Binds are now registered via injector methods instead of returning a list.// Before (4.x) FutureOr<List<Bind<Object>>> binds() => [ Bind.factory<ApiService>((i) => ApiService()), Bind.singleton<DatabaseService>((i) => DatabaseService()), ]; // After (5.x) FutureBinds binds(Injector i) { i.add<ApiService>((i) => ApiService()); i.addSingleton<DatabaseService>((i) => DatabaseService()); }
Added #
- Native injector: Removed dependency on
auto_injector. Direct registration methods:i.add(),i.addSingleton(),i.addLazySingleton(). - Performance: ~4× faster dependency resolution and reduced memory overhead.
- Better error messages: Clearer type mismatch and cycle detection errors.
4.2.2 #
4.2.0+4 #
Fixed #
- Added validation to prevent overwriting already-registered singletons for the same type and key.
4.2.0 #
4.1.0 #
4.0.0 #
Breaking Changes #
-
ModularApp.router: ReplacesMaterialApp.router.routerConfigis set automatically — remove it from your code. -
Async binds and imports:
binds()andimports()now returnFutureOr<List<T>>instead ofList<T>.// Before (3.x) List<Bind<Object>> get binds => [Bind.singleton<MyService>((i) => MyService())]; List<Module> get imports => [SharedModule()]; // After (4.x) FutureOr<List<Bind<Object>>> binds() => [Bind.singleton<MyService>((i) => MyService())]; FutureOr<List<Module>> imports() => [SharedModule()];
Added #
ModularApp.router: ExtendsMaterialApp.routerwith automatic loader display during module registration.ModularLoader: Built-in loading overlay withModularLoader.show()/ModularLoader.hide()and aCustomModularLoaderabstract class for full appearance customisation.
3.0.0 #
Breaking Changes #
-
Root route required: Modules must now declare a root
ChildRoute('/')as their entry point. An assertion error is thrown if it is missing.List<ModularRoute> get routes => [ ChildRoute('/', child: (_, __) => HomePage()), // required ChildRoute('/details', child: (_, __) => DetailsPage()), ];
2.0.3+1 #
2.0.2+1 #
Added #
InternalLogsclass for consistent debug output across route registration and bind management.
Fixed #
_registerwas not being called during route redirects, causing missing dependency injection before page construction.
Improved #
- Bind registration is skipped when no redirect occurs, reducing redundant operations and log noise.