pushed
Scoped dependencies for go_router routes using get_it.
Each route entry on the navigation stack gets its own get_it scope. The scope is created — and its dependencies registered — when the route enters the stack, and disposed when it leaves. Pushing the same route twice yields two independent scopes, and nested routes resolve their parent's dependencies down the scope stack.
Usage
Add pushed to pubspec.yaml:
dependencies:
pushed: ^0.4.0
go_router: ^17.1.0
get_it: ^9.2.1
Create a ScopeObserver, attach it to your GoRouter, and describe each route's scope with withScope:
final observer = ScopeObserver();
final router = GoRouter(
routes: [
GoRoute(
path: '/products',
builder: (context, state) => const ProductsPage(),
).withScope(
observer: observer,
scopeInitializer: (getIt) {
getIt.registerSingleton<ProductService>(ProductService());
},
scopeDisposer: (getIt) async {
await getIt<ProductService>().dispose();
},
),
],
);
observer.attach(router);
attach immediately diffs the router's current navigation state, so entries already present (for example an initial deep link) get their scopes created. Pages then resolve their dependencies from GetIt.instance:
class ProductsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final service = GetIt.instance<ProductService>();
return Text('Hello ${service.name}');
}
}
For a complete working example, see the example app.
Scope lifecycle
On every navigation, the observer diffs routerDelegate.currentConfiguration.matches against the previous stack:
- Matches that entered the stack get their entry scope created synchronously, so dependencies exist before the page builds.
- Matches that left the stack get their scope disposed by name via
GetIt.dropScope, running thescopeDisposer.
Shell scopes (ShellRoute and StatefulShellRoute) are keyed by the shell route object rather than match identity, so the shell's scope survives branch switches and navigation between its children. Attach withScope to the shell route itself; per-branch scoping is not supported.
Best practices
- Use
scopeDisposerfor cleanup. Register the inverse of everyscopeInitializer, or useregisterSingleton(dispose:)— not both, which would dispose the service twice. - Nested routes inherit resolution, not ownership. A child entry resolves its parent's services through the
get_itscope stack, but each entry's scope registers only what that page needs. - Test teardown and logout are different contracts. See the teardown section below.
Teardown
pushed exposes two teardown contracts, on both the observer and the manager: a reset that runs no disposers (test teardown) and a per-ledger disposal that runs them (logout).
scopeObserver.reset() — test teardown, no disposers
Calls GetIt.reset(dispose: false) and clears the observer's live scope state. The recipe registry (.withScope) survives, so the observer keeps creating scopes from the same recipes on the next navigation. The reset is global to the manager's GetIt instance: it wipes scopes owned by other managers sharing it. For tests, inject an isolated GetIt (GetIt.asNewInstance) into the observer's RouteScopeManager so each test starts clean.
scopeObserver.disposeAllScopes() — logout, runs disposers
Runs the scopeDisposer of every scope the observer's manager created, then clears the observer's live state so hasActiveScope reports false immediately. Only this manager's scopes are affected; other managers sharing the same GetIt are untouched. Use for logout or production teardown.
Manager-level teardown
The same two contracts one level down. scopeManager.resetAllScopes() resets the manager's GetIt instance without running disposers; scopeManager.disposeAllScopes() drops this manager's ledger entries by name, running disposers. Prefer the observer-level methods when an observer is attached, since scopeObserver.disposeAllScopes() also clears the observer's bookkeeping.
Troubleshooting
GetIt.get<MyService>() not found
- Make sure the observer is attached:
observer.attach(router). - Register the service in the route's
scopeInitializer. - Verify you are on that route — a scope only exists while its entry is on the stack.
Contributing
Contributions are welcome — see CONTRIBUTING.md. Release history is in CHANGELOG.md.
License
MIT.