Typically, when we need to pass data from a parent Widget to a child Widget, we use Widget constructor parameters. However, our Widget structure is often multi-layered, forming a large Widget Tree. A child Widget at a deeper level may need some data from a distant ancestor Widget. In this case, using only constructor parameters to pass data down the Widget chain level by level can be very cumbersome.
TIP
We can read Flutter Simple State Management - Lifting state up to understand its importance.
Why do we need Oinject?
In the Flutter ecosystem, there are many packages that can do similar things, such as Provider. Even Flutter itself comes with a simple way to provide data: InheritedWidget.
The advantage of Oinject lies in its minimal boilerplate code and ease of use. Here's a simple comparison:
Name | Boilerplate Code | Widget Tree Pollution | Support Data Stacking |
---|---|---|---|
provider | Moderate | Pollutes | Not Supported |
InheritedWidget | High | Pollutes | Not Supported |
oinject | Low | No | Supported |
Most importantly, Oinject can work with any Widget (as long as it has a BuildContext
). This provides great convenience for migrating your existing App to Oinject.
Installation
We run the following command:
flutter pub add oinject
Or add to your pubspec.yaml
:
dependencies:
oinject: latest
Provide
To provide data to Widget descendants, we need to use the provide()
function:
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
provide(context, 'value');
return ...;
}
}
The provide()
function accepts three values and one type parameter. The first parameter is the build context of Flutter Widgets (called BuildContext
, we usually use context
to receive it). The second parameter is the specific value to be provided, which matches the type parameter (pseudocode: <T>(T value)
). The third parameter is the type data Key used for data stacking, designed as a Symbol type:
provide(context, 'value', key: #hello);
The greatest use of Key is to accurately annotate data (relying on type parameters is not reliable) or to stack data of different types:
provide(context, key: #user, 1);
provide(context, key: #user, 'Seven');
With this, we can easily pass the User's ID and Name to descendants. (Only applicable to type data that does not conflict)
Global Provide
In addition to providing data in the Widget tree, you may also want to provide global data at the entry point of the Flutter application. We need to use the provide.global()
function:
void main() {
provide.global('value');
runApp(const App());
}
The only difference between provide.global()
and provide
is that global provision does not require the BuildContext
parameter. It only has two parameters, corresponding to the second and third parameters of provide()
:
void main() {
provide.global(key: #user, 1);
provide.global(key: #user, 'name');
runApp(const App());
}
Inject
To inject data provided by upper-level Widgets, use the inject
function:
class ChildWidget extends StatelessWidget {
Widget build(BuildContext context) {
final name = inject<String>(context);
return ...;
}
}
Injection with Key:
final id = inject<int>(context, #user);
final name = inject<String>(context, #user);
Default Injection Value
By default, inject()
assumes that the passed type parameter or Key ancestor is not provided. Therefore, it will always return a T?
type. If the value you inject is not required to be provided, you can use ??
to set a default value:
final String message = inject(context) ?? 'This is the default message';