The lifecycle of a dependency is determined by where it is registered in the module hierarchy.
Root-owned (Shared DI)
Dependencies registered in a path-less module are committed eagerly at bootstrap and live for the entire duration of the app. These are suitable for "sources of truth" like repositories, services, or app sessions. They are never disposed when navigating.
final coreModule = createModule( // no path → root-owned
register: (c) => c.addSingleton<ProductRepository>(ProductRepository.new),
);
Feature-scoped
Dependencies registered in a module with a path are bound lazily when the feature's first route enters the stack and are automatically disposed when the last route leaves.
Disposal is automatic for ChangeNotifiers and classes implementing Disposable (their dispose() method is called).
final productsModule = createModule(
path: '/products', // feature → binds disposed when it leaves
register: (c) {
c.add<ProductSearchController>(ProductSearchController.new);
// ...routes...
},
);
Decision Guide
- Source of Truth (Repositories, Services, Sessions): Use a root-owned module.
- Feature-local machinery: Use a feature-scoped module.
- Page-specific state: Use page-scoped
provide instead of a module bind.
// Root-owned example
final coreModule = createModule( // no path → root-owned
register: (c) => c.addSingleton<ProductRepository>(ProductRepository.new),
);
// Feature-scoped example
final productsModule = createModule(
path: '/products', // feature → binds disposed when it leaves
register: (c) {
c.add<ProductSearchController>(ProductSearchController.new);
// ...routes...
},
);