From 722a4d7980af0aac58275b126105d6b9eba75e98 Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 8 Sep 2025 09:23:00 +0300 Subject: [PATCH] docs(di): clarify 'toInstance' binding limitations in builder - Add explicit note for users about the impossibility to use scope.resolve() for just-to-be-registered types inside Module.builder when registering chained dependencies via toInstance. - Show correct and incorrect usage patterns, functional and anti-pattern Dart examples in RU and EN full tutorials. - Add the warning to the main README after core concept bindings block, improving discoverability for users starting with the library. - Motivation: Prevent common misuse and hard-to-debug runtime errors for users who construct chains using toInstance/resolve inside the builder. --- cherrypick/README.md | 74 ++++++++++++++++++++++++++++++++--------- doc/full_tutorial_en.md | 40 ++++++++++++++++++++++ doc/full_tutorial_ru.md | 40 ++++++++++++++++++++++ 3 files changed, 138 insertions(+), 16 deletions(-) diff --git a/cherrypick/README.md b/cherrypick/README.md index 27a444d..c9fb1bc 100644 --- a/cherrypick/README.md +++ b/cherrypick/README.md @@ -102,31 +102,73 @@ A **Binding** acts as a configuration for how to create or provide a particular #### Example ```dart -// Provide a direct instance -Binding().toInstance("Hello world"); +void builder(Scope scope) { + // Provide a direct instance + bind().toInstance("Hello world"); -// Provide an async direct instance -Binding().toInstanceAsync(Future.value("Hello world")); + // Provide an async direct instance + bind().toInstanceAsync(Future.value("Hello world")); -// Provide a lazy sync instance using a factory -Binding().toProvide(() => "Hello world"); + // Provide a lazy sync instance using a factory + bind().toProvide(() => "Hello world"); -// Provide a lazy async instance using a factory -Binding().toProvideAsync(() async => "Hello async world"); + // Provide a lazy async instance using a factory + bind().toProvideAsync(() async => "Hello async world"); -// Provide an instance with dynamic parameters (sync) -Binding().toProvideWithParams((params) => "Hello $params"); + // Provide an instance with dynamic parameters (sync) + bind().toProvideWithParams((params) => "Hello $params"); -// Provide an instance with dynamic parameters (async) -Binding().toProvideAsyncWithParams((params) async => "Hello $params"); + // Provide an instance with dynamic parameters (async) + bind().toProvideAsyncWithParams((params) async => "Hello $params"); -// Named instance for retrieval by name -Binding().toProvide(() => "Hello world").withName("my_string"); + // Named instance for retrieval by name + bind().toProvide(() => "Hello world").withName("my_string"); -// Mark as singleton (only one instance within the scope) -Binding().toProvide(() => "Hello world").singleton(); + // Mark as singleton (only one instance within the scope) + bind().toProvide(() => "Hello world").singleton(); +} ``` +> ⚠️ **Important note about using `toInstance` in Module `builder`:** +> +> If you register a chain of dependencies via `toInstance` inside a Module's `builder`, **do not** call `scope.resolve()` for types that are also being registered in the same builder — at the moment they are registered. +> +> CherryPick initializes all bindings in the builder sequentially. Dependencies registered earlier are not yet available to `resolve` within the same builder execution. Trying to resolve just-registered types will result in an error (`Can't resolve dependency ...`). +> +> **How to do it right:** +> Manually construct the full dependency chain before calling `toInstance`: +> +> ```dart +> void builder(Scope scope) { +> final a = A(); +> final b = B(a); +> final c = C(b); +> bind().toInstance(a); +> bind().toInstance(b); +> bind().toInstance(c); +> } +> ``` +> +> **Wrong:** +> ```dart +> void builder(Scope scope) { +> bind().toInstance(A()); +> // Error! At this point, A is not registered yet. +> bind().toInstance(B(scope.resolve())); +> } +> ``` +> +> **Wrong:** +> ```dart +> void builder(Scope scope) { +> bind().toProvide(() => A()); +> // Error! At this point, A is not registered yet. +> bind().toInstance(B(scope.resolve())); +> } +> ``` +> +> **Note:** This limitation applies **only** to `toInstance`. With `toProvide`/`toProvideAsync` and similar providers, you can safely use `scope.resolve()` inside the builder. + ### Module A **Module** is a logical collection point for bindings, designed for grouping and initializing related dependencies. Implement the `builder` method to define how dependencies should be bound within the scope. diff --git a/doc/full_tutorial_en.md b/doc/full_tutorial_en.md index f2e77ec..9d57966 100644 --- a/doc/full_tutorial_en.md +++ b/doc/full_tutorial_en.md @@ -44,6 +44,46 @@ final setupFuture = loadEnvironment(); bind().toInstanceAsync(setupFuture); ``` +> ⚠️ **Important note about using toInstance in Module** +> +> If you register a chain of dependencies via `toInstance` inside the `builder` method of your `Module`, you must NOT call `scope.resolve()` for a type that you have just bound—at this moment. +> +> CherryPick initializes all bindings inside `builder` sequentially: at the time of a new binding, not all other dependencies are registered yet in the DI container. If you try to use `scope.resolve()` for an object you have just added in the same `builder`, it will result in an error (`Can't resolve dependency ...`). +> +> **Correct way:** +> Manually construct the entire object chain before registering: +> +> ```dart +> void builder(Scope scope) { +> final a = A(); +> final b = B(a); +> final c = C(b); +> bind().toInstance(a); +> bind().toInstance(b); +> bind().toInstance(c); +> } +> ``` +> +> **Incorrect:** +> ```dart +> void builder(Scope scope) { +> bind().toInstance(A()); +> // Error! At this point, A is not registered yet. +> bind().toInstance(B(scope.resolve())); +> } +> ``` +> +> **Incorrect:** +> ```dart +> void builder(Scope scope) { +> bind().toProvide(() => A()); +> // Error! At this point, A is not registered yet. +> bind().toInstance(B(scope.resolve())); +> } +> ``` +> +> **Note:** This limitation applies only to `toInstance`. For providers (`toProvide`/`toProvideAsync`) and other strategies, you can freely use `scope.resolve()` inside `builder`. + - **toProvide** — regular sync factory - **toProvideAsync** — async factory (if you need to await a Future) - **toProvideWithParams / toProvideAsyncWithParams** — factories with runtime parameters diff --git a/doc/full_tutorial_ru.md b/doc/full_tutorial_ru.md index 46a0c2d..67c01d3 100644 --- a/doc/full_tutorial_ru.md +++ b/doc/full_tutorial_ru.md @@ -44,6 +44,46 @@ final setupFuture = loadEnvironment(); bind().toInstanceAsync(setupFuture); ``` +> ⚠️ **Важное примечание по использованию toInstance в Module** +> +> Если вы регистрируете цепочку зависимостей через `toInstance` внутри метода `builder` вашего `Module`, нельзя в это же время вызывать `scope.resolve()` для только что объявленного типа. +> +> CherryPick инициализирует биндинги последовательно внутри builder: в этот момент ещё не все зависимости зарегистрированы в DI-контейнере. Попытка воспользоваться `scope.resolve()` для только что добавленного объекта приведёт к ошибке (`Can't resolve dependency ...`). +> +> **Как правильно:** +> Складывайте всю цепочку вручную до регистрации: +> +> ```dart +> void builder(Scope scope) { +> final a = A(); +> final b = B(a); +> final c = C(b); +> bind().toInstance(a); +> bind().toInstance(b); +> bind().toInstance(c); +> } +> ``` +> +> **Неправильно:** +> ```dart +> void builder(Scope scope) { +> bind().toInstance(A()); +> // Ошибка! В этот момент A ещё не зарегистрирован. +> bind().toInstance(B(scope.resolve())); +> } +> ``` +> +> **Неправильно:** +> ```dart +> void builder(Scope scope) { +> bind().toProvide(() => A()); +> // Ошибка! В этот момент A ещё не зарегистрирован. +> bind().toInstance(B(scope.resolve())); +> } +> ``` +> +> **Примечание:** Это ограничение касается только `toInstance`. Для провайдеров (`toProvide`/`toProvideAsync`) и других стратегий вы можете использовать `scope.resolve()` внутри builder без ограничений. + - **toProvide** — обычная синхронная фабрика. - **toProvideAsync** — асинхронная фабрика (например, если нужно дожидаться Future).