diff --git a/doc/full_tutorial_en.md b/doc/full_tutorial_en.md new file mode 100644 index 0000000..7b2e4b6 --- /dev/null +++ b/doc/full_tutorial_en.md @@ -0,0 +1,407 @@ +# 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! + +--- + +## 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)