CherryPick DI
May 7, 2022
CherryPick DI #
DI-container – this is library, when provide mechanism dependency injection.
Content #
- Preface
- Library features
- Library components
- 3.1. scope
- 3.2. module
- 3.3. Binding
- Usage example
- Conclusion
1. Preface #
The first attempts to develop our own DI for pet projects written on the Flutter SDK were started at the start of 2020.
Several reasons prompted me to take this step:
- At that time, I did not find DI in pub.dev with the ability to divide the container into scope (perhaps I was looking badly)
- Simplify working with dependencies in the project
- Desire to write your own DI
- Have a simple DI in your arsenal (hopefully with a simple API)
2. Library’s features: #
DI container’s main features:
- Initialization instance with name
- Initialization instance when singleton
- Separate container to scopes
3. Main components DI #
Library contain three main components:
- Scope
- Module
- Binding
3.1. Scope #
Scope is a container that stores the entire dependency tree (scope, modules, instances).
Through the scope, you can access the custom instance
, for this you need to call the resolve<T>()
method and specify the type of the object, and you can also pass additional parameters.
Example:
// open main scope
final rootScope = Cherrypick.openRootScope();
// initializing scope with a custom module
rootScope.installModules([AppModule()]);
// takes custom instance
final str = rootScope.resolve<String>();
// or
final str = rootScope.tryResolve<String>();
// close main scope
Cherrypick.closeRootScope();
3.2. Module #
Module is a container of user instances, and on the basis of which the user can create their modules. The user in his module must implement the void builder (Scope currentScope)
method.
Example:
class AppModule extends Module {
@override
void builder(Scope currentScope) {
bind<ApiClient>().toInstance(ApiClientMock());
}
}
3.3. Binding #
Binding is a custom instance configurator that contains methods for configuring a dependency.
There are two main methods for initializing a custom instance toInstance ()
and toProvide ()
and auxiliary withName ()
and singleton ()
.
toInstance()
- takes a initialized instance
toProvide()
- takes a provider
function (instance constructor)
withName()
- takes a string to name the instance. By this name, it will be possible to extract instance from the DI container
singleton()
- sets a flag in the Binding that tells the DI container that there is only one dependency.
Example:
// initializing a text string instance through a method toInstance()
Binding<String>().toInstance("hello world");
// or
// initializing a text string instance
Binding<String>().toProvide(() => "hello world");
// initializing an instance of a string named
Binding<String>().withName("my_string").toInstance("hello world");
// or
Binding<String>().withName("my_string").toProvide(() => "hello world");
// instance initialization like singleton
Binding<String>().toInstance("hello world");
// or
Binding<String>().toProvide(() => "hello world").singleton();
4. Example #
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:cherrypick/cherrypick.dart';
class AppModule extends Module {
@override
void builder(Scope currentScope) {
bind<ApiClient>().withName("apiClientMock").toInstance(ApiClientMock());
bind<ApiClient>().withName("apiClientImpl").toInstance(ApiClientImpl());
}
}
class FeatureModule extends Module {
bool isMock;
FeatureModule({required this.isMock});
@override
void builder(Scope currentScope) {
bind<DataRepository>()
.withName("networkRepo")
.toProvide(
() => NetworkDataRepository(
currentScope.resolve<ApiClient>(
named: isMock ? "apiClientMock" : "apiClientImpl",
),
),
)
.singleton();
bind<DataBloc>().toProvide(
() => DataBloc(
currentScope.resolve<DataRepository>(named: "networkRepo"),
),
);
}
}
void main() async {
final scope = openRootScope().installModules([
AppModule(),
]);
final subScope = scope
.openSubScope("featureScope")
.installModules([FeatureModule(isMock: true)]);
final dataBloc = subScope.resolve<DataBloc>();
dataBloc.data.listen((d) => print('Received data: $d'),
onError: (e) => print('Error: $e'), onDone: () => print('DONE'));
await dataBloc.fetchData();
}
class DataBloc {
final DataRepository _dataRepository;
Stream<String> get data => _dataController.stream;
StreamController<String> _dataController = new StreamController.broadcast();
DataBloc(this._dataRepository);
Future<void> fetchData() async {
try {
_dataController.sink.add(await _dataRepository.getData());
} catch (e) {
_dataController.sink.addError(e);
}
}
void dispose() {
_dataController.close();
}
}
abstract class DataRepository {
Future<String> getData();
}
class NetworkDataRepository implements DataRepository {
final ApiClient _apiClient;
final _token = 'token';
NetworkDataRepository(this._apiClient);
@override
Future<String> getData() async => await _apiClient.sendRequest(
url: 'www.google.com', token: _token, requestBody: {'type': 'data'});
}
abstract class ApiClient {
Future sendRequest({@required String url, String token, Map requestBody});
}
class ApiClientMock implements ApiClient {
@override
Future sendRequest(
{@required String? url, String? token, Map? requestBody}) async {
return 'Local Data';
}
}
class ApiClientImpl implements ApiClient {
@override
Future sendRequest(
{@required String? url, String? token, Map? requestBody}) async {
return 'Network data';
}
}
5. Conclusion #
Now this library used to in three commercial projects and own pet projects.