Files
cherrypick/doc/full_tutorial_en.md
Sergey Penkovsky a3648209b9 feat(generator): support output_dir and build_extensions config for generated files
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
2025-07-15 12:07:23 +03:00

14 KiB
Raw Blame History

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 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 via separator argument.

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 profile
  • main.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 @provide become 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 @named annotation 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:
    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

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, 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:

    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:

    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):

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