widject_container 2.0.0
widject_container: ^2.0.0 copied to clipboard
Dependency Injection Container to structure your application in loosely coupled components. Inspired by VContainer.
WidjectContainer #
Dependency Injection package for Flutter
Simple DI package to help structure your Flutter application in loosely coupled components. Inspired by VContainer.
Features #
- Explicit constructor injection within scopes.
- No use of reflection/mirrors.
- Nested scope support to isolate dependencies by widget type.
- Async initialization of scoped dependencies.
- Automatic disposal of registered instances that implement
Disposable, triggered by the widget lifecycle.
Installation #
Add the package with the command: flutter pub add widject_container or adding widject_container to your project's pubspec.yaml dependencies.
Basic Usage #
A scope is itself a widget (ScopeWidget). It defines which dependencies are available within that part of the widget tree. Use install to register types, and addWidget to register the widget that the scope builds.
class AppScope extends ScopeWidget<AppWidget> {
const AppScope({super.key}) : super.createImmediate();
@override
void install(ContainerRegister register) {
register.addWidget((p, key, args) => AppWidget(p.get(), key: key));
register
.add((p) => TapMessageProvider(), Lifetime.transient)
.as<MessageProvider>();
}
}
Where classes are:
abstract class MessageProvider {
String getMessage();
}
class TapMessageProvider implements MessageProvider {
@override
String getMessage() => "Tap Here!";
}
class AppWidget extends StatelessWidget {
final MessageProvider _messageProvider;
const AppWidget(this._messageProvider, {super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WidjectContainer Demo',
home: Scaffold(
body: Text(_messageProvider.getMessage())
)
);
}
}
The scope is the entry point. Pass it directly to runApp:
void main() {
runApp(const AppScope());
}
Widget Resolver #
Use WidgetResolver to instantiate widgets registered within a scope. Dependencies are resolved and explicitly injected, as defined in the scope registration.
Example of registration:
class AppScope extends ScopeWidget<AppWidget> {
...
@override
void install(ContainerRegister register) {
...
register.addWidget((p, key, args) => AppWidget(p.get(), key: key));
}
}
Example of usage through WidgetResolver:
class AppWidget extends StatelessWidget {
final WidgetResolver _widgetResolver;
const AppWidget(this._widgetResolver, {super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WidjectContainer Demo',
home: Scaffold(body: _widgetResolver.resolve<HomeScreenWidget>())
);
}
}
Nested Scopes #
Nest a scope inside a parent scope using addScopeForWidget. The child scope inherits all dependencies registered in ancestor scopes and can define its own. This is ideal for screens or features that require an isolated set of dependencies.
class HomeScope extends ScopeWidget<HomeScreenWidget> {
const HomeScope({super.args, super.key}) : super.createDeferred();
@override
void install(ContainerRegister register) {
register.addWidget((p, key, args) => HomeScreenWidget(p.get(), p.get()));
register
.add((p) => TapMessageProvider(), Lifetime.transient)
.as<MessageProvider>();
register.addScopeForWidget(
(p, key, args) => OtherScreenScope(key: key, args: args));
}
}
In the parent scope, bind the child scope using addScopeForWidget:
class AppScope extends ScopeWidget<AppWidget> {
...
@override
void install(ContainerRegister register) {
...
register.addScopeForWidget(
(p, key, args) => HomeScope(key: key, args: args));
}
}
The child widget is then resolved via WidgetResolver, which automatically wraps the scope around it:
class HomeScreenWidget extends StatelessWidget {
final MessageProvider _messageProvider;
final WidgetResolver _widgetResolver;
const HomeScreenWidget(this._messageProvider, this._widgetResolver, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: TextButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _widgetResolver.resolve<OtherScreenWidget>())),
child: Text(_messageProvider.getMessage())));
}
}
Disposal #
Any registered instance that implements the Disposable interface will have its dispose method called automatically when the scope widget is removed from the widget tree.
class MyService implements Disposable {
@override
void dispose() {
// clean up resources
}
}
Register it as usual within a scope:
register.add((p) => MyService(), Lifetime.singleton).as<MyService>();
Async Initialization #
Register types implementing Initializable to perform async work before the scope's widget is shown. Use createDeferred in the scope constructor to defer rendering until initialization is complete.
class MyInitializable implements Initializable {
@override
InitializationGroup get group => InitializationGroup.normal;
@override
Future initialize() async {
// async setup
}
}
class MyScope extends ScopeWidget<MyWidget> {
const MyScope({super.key}) : super.createDeferred();
@override
void install(ContainerRegister register) {
register.addWidget((p, key, args) => MyWidget(key: key));
register
.add((p) => MyInitializable(), Lifetime.singleton)
.as<Initializable>();
}
}
Debug Logging #
Enable scope lifecycle logs (init, build, dispose) during development via WidjectSettings:
void main() {
WidjectSettings.enableDebugLogs = true;
runApp(const AppScope());
}
Logs are always disabled in release builds regardless of this setting.
Credits #
WidjectContainer is inspired by:
Author #
License #
MIT