Now the code generator supports specifying a custom output directory and extension/name template for generated DI files via build.yaml ( and ). This allows placing all generated code in custom folders and using flexible naming schemes. docs: update all user docs and tutorials to explain new output_dir/build_extensions config - Added detailed usage and YAML examples to cherrypick_generator/README.md - Synced full_tutorial_en.md and full_tutorial_ru.md (advanced codegen section) with explanation of new configuration and impact on imports - Updated quick_start_en.md and quick_start_ru.md to mention advanced customization and point to tutorials - Added troubleshooting and tips for custom output/imports in docs
14 KiB
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— runtime DI corecherrypick_annotations— DI annotationscherrypick_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)
bind<MyService>().toProvide(() => MyServiceImpl());
bind<MyRepository>().toProvideAsync(() async => await initRepo());
bind<UserService>().toProvideWithParams((id) => UserService(id));
// Singleton
bind<MyApi>().toProvide(() => MyApi()).singleton();
// Register an already created object
final config = AppConfig.dev();
bind<AppConfig>().toInstance(config);
// Register an already running Future/async value
final setupFuture = loadEnvironment();
bind<Environment>().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:
bind<ApiClient>().toProvide(() => ApiClientProd()).withName('prod');
bind<ApiClient>().toProvide(() => ApiClientMock()).withName('mock');
// Resolving by name:
final api = scope.resolve<ApiClient>(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:
bind<UserService>().toProvideWithParams((userId) => UserService(userId));
// Resolve:
final userService = scope.resolveWithParams<UserService>(params: '123');
Scope management: dependency hierarchy
For most business cases, a single root scope is enough, but CherryPick supports nested scopes:
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:
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 viaseparatorargument.
Example with another separator:
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 profilemain.profile.details— even narrower context
Closing subscopes
To close a specific subScope, use the same path:
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:
// 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:
final service = scope.tryResolve<OptionalService>(); // 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
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
@providebecome DI factories. - Add other annotations to specify binding type or name.
Generated code will look like:
class $AppModule extends AppModule {
@override
void builder(Scope currentScope) {
bind<ApiClient>().toProvide(() => apiClient()).singleton();
bind<UserService>().toProvide(() => userService(currentScope.resolve<ApiClient>()));
bind<ApiClient>().toProvide(() => mockApiClient()).withName('mock').singleton();
}
}
Example: field injection
@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
@namedannotation links a field to a named implementation.
Example generated code:
mixin $ProfileBloc {
@override
void _inject(ProfileBloc instance) {
instance.auth = CherryPick.openRootScope().resolve<AuthService>();
instance.adminUser = CherryPick.openRootScope().resolve<UserService>(named: 'admin');
}
}
How to connect it
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:
bind<RemoteConfig>().toProvideAsync(() async => await RemoteConfig.load());
// Usage:
final config = await scope.resolveAsync<RemoteConfig>();
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 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:
UseCherryPickProviderto access rootScope and subscopes anywhere in the widget tree. - Context integration:
UseCherryPickProvider.of(context)for DI access inside your widgets.
Usage Example
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<AppService>().getStatus(),
),
),
),
);
}
}
- Here,
CherryPickProviderwraps 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
-
Add dependencies:
dependencies: cherrypick: ^1.0.0 cherrypick_annotations: ^1.0.0 dev_dependencies: build_runner: ^2.0.0 cherrypick_generator: ^1.0.0 -
Describe your modules using annotations.
-
To generate DI code:
dart run build_runner build --delete-conflicting-outputs -
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):
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.:
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 |