ioc_widget
A simple, flexible, and testable dependency injection (DI) solution for Flutter, inspired by the best of both the Provider and get_it packages. This package is designed for widget-level dependency injection, supporting both transient (injectable) and singleton (lazy) strategies, and is ideal for managing dependencies in a scalable, testable, and maintainable way.
Motivation
We already have the Provider package that somehow handles dependency injection and it's perfect.
My only issue with Provider is the easy misuse of it. When you go in the Flutter official docs, they suggest using Provider as a simple state manegement package alongsie the fact that it can work as a dependency injection mechanism.
However in the guide, it's suggested that you have all the list of your providers on the root level of your app, this results in all provided classes to be singletons which is not a realistic case scenario.
So, how about we have something with the same simplicity as Provider but it can provide singletons and new instances everytime you get the dependency just like get_it?
Why use ioc_widget?
Use this package if you want:
- Explicit DI: Register and scope dependencies in the widget tree, not globally.
- Testability: Easily override dependencies in tests.
- Lifecycle control: Choose between transient (new instance per request) and lazy singleton (one instance per scope) strategies.
- No magic: No code generation, no global singletons, no hidden state.
Getting Started
Add to your pubspec.yaml:
dependencies:
ioc_widget: ^<latest_version>
Core Concepts
- InjectableWidget: Provides a new instance of a dependency every time it is requested.
- LazySingletonWidget: Provides a single instance of a dependency for the subtree, created on first use.
- MultiIocWidget: Register multiple dependencies at once.
- IocConsumer: Injects dependencies in context widget tree and handles it's dispose callback.
- context.get: Retrieve a dependency of type
Tfrom the nearest provider.
Usage Example
import 'package:flutter/material.dart';
import 'package:ioc_widget/ioc_widget.dart';
class ClassA {
String talk() => "I'm Class A! $hashCode";
}
class ClassB {
final ClassA classA;
ClassB(this.classA);
String talk() => "I'm Class B! $hashCode\nAnd I'm Class A! ${classA.hashCode}";
}
class ClassC {
final ClassB classB;
ClassC(this.classB);
String talk() => "I'm Class C! $hashCode\n" + classB.talk();
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
'/': (_) => const PageA(),
'/b': (_) => const PageB(),
'/c': (_) => const PageC(),
},
builder: (_, child) => MultiIocWidget(
dependencies: [
InjectableWidget<ClassA>(factory: (_) => ClassA()),
LazySingletonWidget<ClassB>(factory: (ctx) => ClassB(ctx.get())),
LazySingletonWidget<ClassC>(factory: (ctx) => ClassC(ctx.get())),
],
child: child!,
),
);
}
}
class PageA extends StatelessWidget {
const PageA({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page A - InjectableWidget')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('ClassA is injected as a transient.'),
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('ClassA: ${context.get<ClassA>().talk()}')),
);
},
child: const Text('Talk (ClassA)'),
),
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/b'),
child: const Text('Go to Page B'),
),
],
),
),
);
}
}
class PageB extends StatelessWidget {
const PageB({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page B - LazySingletonWidget')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('ClassB is injected as a lazy singleton.'),
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('ClassB: ${context.get<ClassB>().talk()}')),
);
},
child: const Text('Talk (ClassB)'),
),
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/c'),
child: const Text('Go to Page C'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Back to Page A'),
),
],
),
),
);
}
}
class PageC extends StatelessWidget {
const PageC({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Page C - IocConsumer')),
body: Center(
child: IocConsumer<ClassA>(
builder: (ctx) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('ClassC is injected using IocConsumer.'),
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(content: Text('ClassC: ${ctx.get<ClassC>().talk()}')),
);
},
child: const Text('Talk (ClassC)'),
),
ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(ctx).showSnackBar(
SnackBar(content: Text('ClassA: ${ctx.get<ClassA>().talk()}')),
);
},
child: const Text('Talk (ClassA)'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('Back to Page B'),
),
],
);
},
),
),
);
}
}
API Reference
InjectableWidget
Provides a new instance of T every time it is requested from the context.
LazySingletonWidget
Provides a single instance of T for the subtree, created on first use.
MultiIocWidget
Registers multiple dependencies at once. Useful for grouping related dependencies.
IocConsumer
Widget that exposes a dependency in its context widget tree as a singleton and gets disposed when it's parent is disposed.
context.get
Extension on BuildContext to retrieve a dependency of type T from the nearest provider.
Testing
The package is designed for testability. You can easily override dependencies in your widget tests. Example:
testWidgets('InjectableWidget creates a new instance every time', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: InjectableWidget<TestClass>(
factory: (_) => TestClass(),
child: Builder(
builder: (context) {
final a = context.get<TestClass>();
final b = context.get<TestClass>();
return Text('${a.hashCode}-${b.hashCode}');
},
),
),
),
);
expect(TestClass.instanceCount, 2);
});
// See test/ioc_widget_test.dart for more scenarios.
Lifecycle & Disposal
LazySingletonWidgetsupports adisposecallback for cleaning up resources when the widget is removed from the tree.InjectableWidgetdoes not calldispose(since it creates new instances each time) so you should either explicitly call the dispose method of that specific instance or safely dispose it by mixing it with theIocConsumerwidget.
When to use
- When you want explicit, widget-scoped dependency injection.
- When you want to avoid global singletons and make your app more testable.
- When you want to control the lifecycle of your dependencies.
When NOT to use
- If you want global, app-wide singletons (use get_it directly).
- If you want state management (use Provider, Riverpod, Bloc, etc.).
License
MIT