# Full Guide to CherryPick DI for Dart and Flutter: Dependency Injection with Annotations and Automatic Code Generation **CherryPick** is a powerful tool for dependency injection in Dart and Flutter projects. It offers a modern approach with code generation, async providers, named and parameterized bindings, and field injection using annotations. > Tools: > - [`cherrypick`](https://pub.dev/packages/cherrypick) β€” runtime DI core > - [`cherrypick_annotations`](https://pub.dev/packages/cherrypick_annotations) β€” DI annotations > - [`cherrypick_generator`](https://pub.dev/packages/cherrypick_generator) β€” DI code generation > --- ## CherryPick advantages vs other DI frameworks - πŸ“¦ Simple declarative API for registering and resolving dependencies - ⚑️ Full support for both sync and async registrations - 🧩 DI via annotations with codegen, including advanced field injection - 🏷️ Named bindings for multiple interface implementations - 🏭 Parameterized bindings for runtime factories (e.g., by ID) - 🌲 Flexible scope system for dependency isolation and hierarchy - πŸ•ΉοΈ Optional resolution (`tryResolve`) - 🐞 Clear compile-time errors for invalid annotation or DI configuration --- ## How CherryPick works: core concepts ### Dependency registration (bindings) ```dart bind().toProvide(() => MyServiceImpl()); bind().toProvideAsync(() async => await initRepo()); bind().toProvideWithParams((id) => UserService(id)); // Singleton bind().toProvide(() => MyApi()).singleton(); // Register an already created object final config = AppConfig.dev(); bind().toInstance(config); // Register an already running Future/async value final setupFuture = loadEnvironment(); bind().toInstanceAsync(setupFuture); ``` - **toProvide** β€” regular sync factory - **toProvideAsync** β€” async factory (if you need to await a Future) - **toProvideWithParams / toProvideAsyncWithParams** β€” factories with runtime parameters - **toInstance** β€” registers an already created object as a dependency - **toInstanceAsync** β€” registers an already started Future as an async dependency ### Named bindings You can register several implementations of an interface under different names: ```dart bind().toProvide(() => ApiClientProd()).withName('prod'); bind().toProvide(() => ApiClientMock()).withName('mock'); // Resolving by name: final api = scope.resolve(named: 'mock'); ``` ### Lifecycle: singleton - `.singleton()` β€” single instance per Scope lifetime - By default, every resolve creates a new object ### Parameterized bindings Allows you to create dependencies with runtime parameters, e.g., a service for a user with a given ID: ```dart bind().toProvideWithParams((userId) => UserService(userId)); // Resolve: final userService = scope.resolveWithParams(params: '123'); ``` --- ## Scope management: dependency hierarchy For most business cases, a single root scope is enough, but CherryPick supports nested scopes: ```dart final rootScope = CherryPick.openRootScope(); final profileScope = rootScope.openSubScope('profile') ..installModules([ProfileModule()]); ``` - **Subscope** can override parent dependencies. - When resolving, first checks its own scope, then up the hierarchy. ## Managing names and scope hierarchy (subscopes) in CherryPick CherryPick supports nested scopes, each can be "root" or a child. For accessing/managing the hierarchy, CherryPick uses scope names (strings) as well as convenient open/close methods. ### Open subScope by name CherryPick uses separator-delimited strings to search and build scope trees, for example: ```dart final subScope = CherryPick.openScope(scopeName: 'profile.settings'); ``` - Here, `'profile.settings'` will open 'profile' subscope in root, then 'settings' subscope in 'profile'. - Default separator is a dot (`.`), can be changed via `separator` argument. **Example with another separator:** ```dart final subScope = CherryPick.openScope( scopeName: 'project>>dev>>api', separator: '>>', ); ``` ### Hierarchy & access Each hierarchy level is a separate scope. This is convenient for restricting/localizing dependencies, for example: - `main.profile` β€” dependencies only for user profile - `main.profile.details` β€” even narrower context ### Closing subscopes To close a specific subScope, use the same path: ```dart CherryPick.closeScope(scopeName: 'profile.settings'); ``` - Closing a top-level scope (`profile`) wipes all children too. ### Methods summary | Method | Description | |---------------------------|--------------------------------------------------------| | `openRootScope()` | Open/get root scope | | `closeRootScope()` | Close root scope, remove all dependencies | | `openScope(scopeName)` | Open scope(s) by name & hierarchy (`'a.b.c'`) | | `closeScope(scopeName)` | Close specified scope or subScope | --- **Recommendations:** Use meaningful names and dot notation for scope structuring in large appsβ€”this improves readability and dependency management on any level. --- **Example:** ```dart // Opens scopes by hierarchy: app -> module -> page final scope = CherryPick.openScope(scopeName: 'app.module.page'); // Closes 'module' and all nested subscopes CherryPick.closeScope(scopeName: 'app.module'); ``` --- This lets you scale CherryPick DI for any app complexity! --- ## Safe dependency resolution If not sure a dependency exists, use tryResolve/tryResolveAsync: ```dart final service = scope.tryResolve(); // returns null if not exists ``` --- ## Dependency injection with annotations & code generation CherryPick supports DI with annotations, letting you eliminate manual DI setup. ### Annotation structure | Annotation | Purpose | Where to use | |---------------|---------------------------|------------------------------------| | `@module` | DI module | Classes | | `@singleton` | Singleton | Module methods | | `@instance` | New object | Module methods | | `@provide` | Provider | Methods (with DI params) | | `@named` | Named binding | Method argument/Class fields | | `@params` | Parameter passing | Provider argument | | `@injectable` | Field injection support | Classes | | `@inject` | Auto-injection | Class fields | | `@scope` | Scope/realm | Class fields | ### Example DI module ```dart import 'package:cherrypick_annotations/cherrypick_annotations.dart'; @module() abstract class AppModule extends Module { @singleton() @provide() ApiClient apiClient() => ApiClient(); @provide() UserService userService(ApiClient api) => UserService(api); @singleton() @provide() @named('mock') ApiClient mockApiClient() => ApiClientMock(); } ``` - Methods annotated with `@provide` become DI factories. - Add other annotations to specify binding type or name. Generated code will look like: ```dart class $AppModule extends AppModule { @override void builder(Scope currentScope) { bind().toProvide(() => apiClient()).singleton(); bind().toProvide(() => userService(currentScope.resolve())); bind().toProvide(() => mockApiClient()).withName('mock').singleton(); } } ``` ### Example: field injection ```dart @injectable() class ProfileBloc with _$ProfileBloc { @inject() late final AuthService auth; @inject() @named('admin') late final UserService adminUser; ProfileBloc() { _inject(this); // injectFields β€” generated method } } ``` - Generator creates a mixin (`_$ProfileBloc`) which automatically resolves and injects dependencies into fields. - The `@named` annotation links a field to a named implementation. Example generated code: ```dart mixin $ProfileBloc { @override void _inject(ProfileBloc instance) { instance.auth = CherryPick.openRootScope().resolve(); instance.adminUser = CherryPick.openRootScope().resolve(named: 'admin'); } } ``` ### How to connect it ```dart void main() async { final scope = CherryPick.openRootScope(); scope.installModules([ $AppModule(), ]); // DI via field injection final bloc = ProfileBloc(); runApp(MyApp(bloc: bloc)); } ``` --- ## Async dependencies For async providers, use `toProvideAsync`, and resolve them with `resolveAsync`: ```dart bind().toProvideAsync(() async => await RemoteConfig.load()); // Usage: final config = await scope.resolveAsync(); ``` --- ## Validation and diagnostics - If you use incorrect annotations or DI config, you'll get clear compile-time errors. - Binding errors are found during code generation, minimizing runtime issues and speeding up development. --- ## Flutter integration: cherrypick_flutter ### What it is [`cherrypick_flutter`](https://pub.dev/packages/cherrypick_flutter) is the integration package for CherryPick DI in Flutter. It provides a convenient `CherryPickProvider` widget which sits in your widget tree and gives access to the root DI scope (and subscopes) from context. ### Features - **Global DI Scope Access:** Use `CherryPickProvider` to access rootScope and subscopes anywhere in the widget tree. - **Context integration:** Use `CherryPickProvider.of(context)` for DI access inside your widgets. ### Usage Example ```dart import 'package:flutter/material.dart'; import 'package:cherrypick_flutter/cherrypick_flutter.dart'; void main() { runApp( CherryPickProvider( child: MyApp(), ), ); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final rootScope = CherryPickProvider.of(context).openRootScope(); return MaterialApp( home: Scaffold( body: Center( child: Text( rootScope.resolve().getStatus(), ), ), ), ); } } ``` - Here, `CherryPickProvider` wraps the app and gives DI scope access via context. - You can create subscopes, e.g. for screens or modules: `final subScope = CherryPickProvider.of(context).openSubScope(scopeName: "profileFeature");` --- ## CherryPick is not just for Flutter! You can use CherryPick in Dart CLI, server apps, and microservices. All major features work without Flutter. --- ## CherryPick Example Project: Step by Step 1. Add dependencies: ```yaml dependencies: cherrypick: ^1.0.0 cherrypick_annotations: ^1.0.0 dev_dependencies: build_runner: ^2.0.0 cherrypick_generator: ^1.0.0 ``` 2. Describe your modules using annotations. 3. To generate DI code: ```shell dart run build_runner build --delete-conflicting-outputs ``` 4. Enjoy modern DI with no boilerplate! --- ### Advanced: Customizing Generated Code Location CherryPick's code generator now supports flexible output configuration via `build.yaml`. You can control both the output directory (using `output_dir`) and filename templates (using `build_extensions`): ```yaml targets: $default: builders: cherrypick_generator|inject_generator: options: build_extensions: '^lib/app.dart': ['lib/generated/app.inject.cherrypick.g.dart'] output_dir: lib/generated generate_for: - lib/**.dart cherrypick_generator|module_generator: options: build_extensions: '^lib/di/{{}}.dart': ['lib/generated/di/{{}}.module.cherrypick.g.dart'] output_dir: lib/generated generate_for: - lib/**.dart ``` - **output_dir**: Folder where all generated files will be placed. - **build_extensions**: Allows full customization of generated file names and subfolders. If you use these, be sure to update your imports accordingly, e.g.: ```dart import 'package:your_project/generated/app.inject.cherrypick.g.dart'; ``` If not specified, generated files will appear next to your source files, as before. --- --- ## Conclusion **CherryPick** is a modern DI solution for Dart and Flutter, combining a concise API and advanced annotation/codegen features. Scopes, parameterized providers, named bindings, and field-injection make it great for both small and large-scale projects. **Full annotation list and their purposes:** | Annotation | Purpose | Where to use | |---------------|---------------------------|------------------------------------| | `@module` | DI module | Classes | | `@singleton` | Singleton | Module methods | | `@instance` | New object | Module methods | | `@provide` | Provider | Methods (with DI params) | | `@named` | Named binding | Method argument/Class fields | | `@params` | Parameter passing | Provider argument | | `@injectable` | Field injection support | Classes | | `@inject` | Auto-injection | Class fields | | `@scope` | Scope/realm | Class fields | --- ## Useful Links - [cherrypick](https://pub.dev/packages/cherrypick) - [cherrypick_annotations](https://pub.dev/packages/cherrypick_annotations) - [cherrypick_generator](https://pub.dev/packages/cherrypick_generator) - [Sources on GitHub](https://github.com/pese-git/cherrypick)