docs(binding,docs): explain .singleton() + parametric provider behavior

- Add an explicit warning and usage examples for .singleton() combined with toProvideWithParams/toProvideAsyncWithParams:
  - in API doc-comment for singleton() in binding.dart,
  - in README.md and both full tutorials (EN/RU).
- Show correct and incorrect usage/pitfalls for parameterized providers and singleton.
- Help users avoid unintended singleton caching when using providers with parameters.
- Motivation: Prevent common confusion, make advanced DI scenarios safer and more obvious.
This commit is contained in:
Sergey Penkovsky
2025-09-08 10:18:19 +03:00
parent 482b7b0f5f
commit b5b672765e
4 changed files with 73 additions and 0 deletions

View File

@@ -169,6 +169,22 @@ void builder(Scope scope) {
>
> **Note:** This limitation applies **only** to `toInstance`. With `toProvide`/`toProvideAsync` and similar providers, you can safely use `scope.resolve<T>()` inside the builder.
> ⚠️ **Special note regarding `.singleton()` with `toProvideWithParams()` / `toProvideAsyncWithParams()`:**
>
> If you declare a binding using `.toProvideWithParams(...)` (or its async variant) and then chain `.singleton()`, only the **very first** `resolve<T>(params: ...)` will use its parameters; every subsequent call (regardless of params) will return the same (cached) instance.
>
> **Example:**
> ```dart
> bind<Service>().toProvideWithParams((params) => Service(params)).singleton();
> final a = scope.resolve<Service>(params: 1); // creates Service(1)
> final b = scope.resolve<Service>(params: 2); // returns Service(1)
> print(identical(a, b)); // true
> ```
>
> Use this pattern only when you want a “master” singleton. If you expect a new instance per params, **do not** use `.singleton()` on parameterized providers.
### 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.

View File

@@ -241,6 +241,23 @@ class Binding<T> {
/// ```dart
/// bind<Api>().toProvide(() => MyApi()).singleton();
/// ```
///
/// ---
///
/// ⚠️ **Special note: Behavior with parametric providers (`toProvideWithParams`/`toProvideAsyncWithParams`):**
///
/// If you declare a binding using `.toProvideWithParams(...)` (or its async variant) and then chain `.singleton()`, only the **very first** `resolve<T>(params: ...)` will use its parameters;
/// every subsequent call (regardless of params) will return the same (cached) instance.
///
/// Example:
/// ```dart
/// bind<Service>().toProvideWithParams((params) => Service(params)).singleton();
/// final a = scope.resolve<Service>(params: 1); // creates Service(1)
/// final b = scope.resolve<Service>(params: 2); // returns Service(1)
/// print(identical(a, b)); // true
/// ```
///
/// Use this pattern only if you want a master singleton. If you expect a new instance per params, **do not** use `.singleton()` on parameterized providers.
Binding<T> singleton() {
_resolver?.toSingleton();
return this;

View File

@@ -118,6 +118,26 @@ bind<UserService>().toProvideWithParams((userId) => UserService(userId));
final userService = scope.resolve<UserService>(params: '123');
```
> ⚠️ **Special note on using `.singleton()` after `toProvideWithParams` or `toProvideAsyncWithParams`:**
>
> If you declare a binding using `.toProvideWithParams((params) => ...)` (or its async variant) and then call `.singleton()`, the DI container will create and cache **only one instance** on the first `resolve` call—with the initial parameters. All subsequent calls to `resolve<T>(params: ...)` will return that same (cached) instance, **regardless of the new parameters**.
>
> **Example:**
> ```dart
> bind<Service>().toProvideWithParams((params) => Service(params)).singleton();
>
> final a = scope.resolve<Service>(params: 1); // Creates Service(1)
> final b = scope.resolve<Service>(params: 2); // Returns Service(1)
> print(identical(a, b)); // true
> ```
>
> In other words:
> - The provider function receives parameters only on its first call,
> - After that, no matter what parameters are passed, the same instance is always returned.
>
> **Recommendation:**
> Use `.singleton()` with parameterized providers only if you are sure all parameters should always be identical, or you intentionally want a “master” instance. Otherwise, omit `.singleton()` to ensure a new object is constructed for every unique `params` value.
---
## Scope management: dependency hierarchy

View File

@@ -119,6 +119,26 @@ bind<UserService>().toProvideWithParams((userId) => UserService(userId));
final userService = scope.resolve<UserService>(params: '123');
```
> ⚠️ **Особенности использования `.singleton()` после `toProvideWithParams` или `toProvideAsyncWithParams`:**
>
> Если вы объявляете биндинг через `.toProvideWithParams((params) => ...)` (или асинхронный вариант) и затем вызываете `.singleton()`, DI-контейнер создаст и закэширует **только один экземпляр** при первом вызове `resolve` — с первыми переданными параметрами. Все последующие вызовы `resolve<T>(params: ...)` вернут этот же (кэшированный) объект **независимо от новых параметров**.
>
> **Пример:**
> ```dart
> bind<Service>().toProvideWithParams((params) => Service(params)).singleton();
>
> final a = scope.resolve<Service>(params: 1); // Создаётся Service(1)
> final b = scope.resolve<Service>(params: 2); // Возвращается уже Service(1)
> print(identical(a, b)); // true
> ```
>
> То есть:
> - параметры работают только для первого вызова,
> - дальше всегда возвращается экземпляр, созданный при первом обращении.
>
> **Рекомендация:**
> Используйте `.singleton()` совместно с провайдерами с параметрами только тогда, когда вы точно уверены, что все параметры всегда должны совпадать, или нужны именно “мастер”-экземпляры. В противном случае не используйте `.singleton()`, чтобы каждый вызов с новыми parameters создавал новый объект.
---
## Управление Scope'ами: иерархия зависимостей