Files
cherrypick/doc/quick_start_en.md
Sergey Penkovsky a9c95f6a89 docs+feat: add Disposable interface source and usage example
feat(core,doc): unified async dispose mechanism for resource cleanup

BREAKING CHANGE:

- Added full support for asynchronous resource cleanup via a unified FutureOr<void> dispose() method in the Disposable interface.
- The Scope now provides only Future<void> dispose() for disposing all tracked resources and child scopes (sync-only dispose() was removed).
- All calls to cleanup in code and tests (scope itself, subscopes, and custom modules) now require await ...dispose().
- Documentation and all examples updated: resource management is always async and must be awaited; Disposable implementers may use both sync and async cleanup.
- Old-style, synchronous cleanup methods have been completely removed (API is now consistently async for all DI lifecycle management).
- Example and tutorial code now demonstrate async resource disposal patterns.
2025-08-08 16:08:29 +03:00

6.8 KiB

Quick start

Main components DI

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();

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());
  }
}

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>();

    // Recommended: Close the root scope & automatically release all Disposable resources
    await Cherrypick.closeRootScope();
    // Or, for advanced/manual scenarios:
    // await rootScope.dispose();

Automatic resource management (Disposable, dispose)

If your service implements the Disposable interface, CherryPick will automatically await dispose() when you close a scope.

Best practice:
Always finish your work with await Cherrypick.closeRootScope() (for the root scope) or await scope.closeSubScope('feature') (for subscopes).
These methods will automatically await dispose() on all resolved objects implementing Disposable, ensuring safe and complete cleanup (sync and async).

Manual await scope.dispose() is available if you manage scopes yourself.

Example

class MyService implements Disposable {
  @override
  FutureOr<void> dispose() async {
    // release resources, close connections, perform async shutdown, etc.
    print('MyService disposed!');
  }
}

final scope = openRootScope();
scope.installModules([
  ModuleImpl(),
]);

final service = scope.resolve<MyService>();

// ... use service

// Recommended:
await Cherrypick.closeRootScope(); // will print: MyService disposed!

// Or, to close a subscope:
await scope.closeSubScope('feature');

class ModuleImpl extends Module {
  @override
  void builder(Scope scope) {
    bind<MyService>().toProvide(() => MyService()).singleton();
  }
}

Logging

To enable logging of all dependency injection (DI) events and errors in CherryPick, set the global logger before creating your scopes:

import 'package:cherrypick/cherrypick.dart';

void main() {
  // Set a global logger before any scopes are created
  CherryPick.setGlobalLogger(PrintLogger()); // or your own custom logger
  final scope = CherryPick.openRootScope();
  // All DI events and cycle errors will now be sent to your logger
}
  • By default, CherryPick uses SilentLogger (no output in production).
  • Any dependency resolution, scope events, or cycle detection errors are logged via info/error on your logger.

Example app

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';
  }
}