Files
cherrypick/cherrypick/README.md

363 lines
11 KiB
Markdown
Raw Normal View History

2025-05-19 16:06:51 +03:00
# CherryPick
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
`cherrypick` is a flexible and lightweight dependency injection library for Dart and Flutter. It provides an easy-to-use system for registering, scoping, and resolving dependencies using modular bindings and hierarchical scopes. The design enables cleaner architecture, testability, and modular code in your applications.
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
## Key Concepts
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
### Binding
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
A **Binding** acts as a configuration for how to create or provide a particular dependency. Bindings support:
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
- Direct instance assignment (`toInstance()`, `toInstanceAsync()`)
- Lazy providers (sync/async functions)
- Provider functions supporting dynamic parameters
- Named instances for resolving by string key
- Optional singleton lifecycle
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
#### Example
2021-04-23 08:43:10 +03:00
```dart
2025-05-19 16:06:51 +03:00
// Provide a direct instance
Binding<String>().toInstance("Hello world");
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
// Provide an async direct instance
Binding<String>().toInstanceAsync(Future.value("Hello world"));
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
// Provide a lazy sync instance using a factory
Binding<String>().toProvide(() => "Hello world");
2025-05-13 23:36:44 +03:00
2025-05-19 16:06:51 +03:00
// Provide a lazy async instance using a factory
Binding<String>().toProvideAsync(() async => "Hello async world");
2025-05-13 23:36:44 +03:00
2025-05-19 16:06:51 +03:00
// Provide an instance with dynamic parameters (sync)
Binding<String>().toProvideWithParams((params) => "Hello $params");
2025-05-13 23:36:44 +03:00
2025-05-19 16:06:51 +03:00
// Provide an instance with dynamic parameters (async)
Binding<String>().toProvideAsyncWithParams((params) async => "Hello $params");
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
// Named instance for retrieval by name
Binding<String>().toProvide(() => "Hello world").withName("my_string");
// Mark as singleton (only one instance within the scope)
Binding<String>().toProvide(() => "Hello world").singleton();
2021-04-23 08:43:10 +03:00
```
2025-05-19 16:06:51 +03:00
### Module
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
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.
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
#### Example
2021-04-23 08:43:10 +03:00
```dart
class AppModule extends Module {
@override
void builder(Scope currentScope) {
bind<ApiClient>().toInstance(ApiClientMock());
2025-05-19 16:06:51 +03:00
bind<String>().toProvide(() => "Hello world!");
2021-04-23 08:43:10 +03:00
}
}
```
2025-05-19 16:06:51 +03:00
### Scope
A **Scope** manages a tree of modules and dependency instances. Scopes can be nested into hierarchies (parent-child), supporting modular app composition and context-specific overrides.
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
You typically work with the root scope, but can also create named subscopes as needed.
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
#### Example
2021-04-23 08:43:10 +03:00
```dart
2025-05-19 16:06:51 +03:00
// Open the main/root scope
2025-05-13 23:36:44 +03:00
final rootScope = CherryPick.openRootScope();
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
// Install a custom module
2025-05-02 11:46:47 +03:00
rootScope.installModules([AppModule()]);
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
// Resolve a dependency synchronously
2025-05-02 11:46:47 +03:00
final str = rootScope.resolve<String>();
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
// Resolve a dependency asynchronously
final result = await rootScope.resolveAsync<String>();
2025-05-13 23:36:44 +03:00
2025-05-19 16:06:51 +03:00
// Close the root scope once done
2025-05-13 23:36:44 +03:00
CherryPick.closeRootScope();
2021-04-23 08:43:10 +03:00
```
2025-05-19 16:06:51 +03:00
#### Working with Subscopes
```dart
// Open a named child scope (e.g., for a feature/module)
final subScope = rootScope.openSubScope('featureScope')
..installModules([FeatureModule()]);
// Resolve from subScope, with fallback to parents if missing
final dataBloc = await subScope.resolveAsync<DataBloc>();
```
### Fast Dependency Lookup (Performance Improvement)
> **Performance Note:**
> **Starting from version 3.0.0**, CherryPick uses a Map-based resolver index for dependency lookup. This means calls to `resolve<T>()` and related methods are now O(1) operations, regardless of the number of modules or bindings in your scope. Previously, the library had to iterate over all modules and bindings to locate the requested dependency, which could impact performance as your project grew.
>
> This optimization is internal and does not change any library APIs or usage patterns, but it significantly improves resolution speed in larger applications.
2025-05-19 16:06:51 +03:00
### Dependency Lookup API
- `resolve<T>()` — Locates a dependency instance or throws if missing.
- `resolveAsync<T>()` — Async variant for dependencies requiring async binding.
- `tryResolve<T>()` — Returns `null` if not found (sync).
- `tryResolveAsync<T>()` — Returns `null` async if not found.
Supports:
- Synchronous and asynchronous dependencies
- Named dependencies
- Provider functions with and without runtime parameters
2025-05-02 11:46:47 +03:00
## Example Application
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
Below is a complete example illustrating modules, subscopes, async providers, and dependency resolution.
2021-04-23 08:43:10 +03:00
```dart
import 'dart:async';
import 'package:meta/meta.dart';
2021-10-20 09:17:10 +03:00
import 'package:cherrypick/cherrypick.dart';
2021-04-23 08:43:10 +03:00
class AppModule extends Module {
@override
void builder(Scope currentScope) {
bind<ApiClient>().withName("apiClientMock").toInstance(ApiClientMock());
bind<ApiClient>().withName("apiClientImpl").toInstance(ApiClientImpl());
}
}
class FeatureModule extends Module {
2025-05-13 23:36:44 +03:00
final bool isMock;
2021-04-23 08:43:10 +03:00
FeatureModule({required this.isMock});
@override
void builder(Scope currentScope) {
2025-05-19 16:06:51 +03:00
// Async provider for DataRepository with named dependency selection
2021-04-23 08:43:10 +03:00
bind<DataRepository>()
.withName("networkRepo")
2025-05-13 23:36:44 +03:00
.toProvideAsync(() async {
final client = await Future.delayed(
2025-05-19 16:06:51 +03:00
Duration(milliseconds: 100),
() => currentScope.resolve<ApiClient>(
named: isMock ? "apiClientMock" : "apiClientImpl",
),
);
2025-05-13 23:36:44 +03:00
return NetworkDataRepository(client);
})
2021-12-10 22:04:51 +03:00
.singleton();
2025-05-19 16:06:51 +03:00
// Chained async provider for DataBloc
2025-05-13 23:36:44 +03:00
bind<DataBloc>().toProvideAsync(
() async {
2025-05-19 16:06:51 +03:00
final repo = await currentScope.resolveAsync<DataRepository>(
named: "networkRepo");
2025-05-13 23:36:44 +03:00
return DataBloc(repo);
},
2021-04-23 08:43:10 +03:00
);
}
}
void main() async {
2025-05-19 16:06:51 +03:00
final scope = CherryPick.openRootScope().installModules([AppModule()]);
final featureScope = scope.openSubScope("featureScope")
..installModules([FeatureModule(isMock: true)]);
2021-04-23 08:43:10 +03:00
2025-05-19 16:06:51 +03:00
final dataBloc = await featureScope.resolveAsync<DataBloc>();
dataBloc.data.listen(
(d) => print('Received data: $d'),
onError: (e) => print('Error: $e'),
onDone: () => print('DONE'),
);
2021-04-23 08:43:10 +03:00
await dataBloc.fetchData();
}
class DataBloc {
final DataRepository _dataRepository;
Stream<String> get data => _dataController.stream;
2025-05-19 16:06:51 +03:00
final StreamController<String> _dataController = StreamController.broadcast();
2021-04-23 08:43:10 +03:00
DataBloc(this._dataRepository);
Future<void> fetchData() async {
try {
_dataController.sink.add(await _dataRepository.getData());
} catch (e) {
_dataController.sink.addError(e);
}
}
void dispose() {
_dataController.close();
}
}
abstract class DataRepository {
Future<String> getData();
}
class NetworkDataRepository implements DataRepository {
final ApiClient _apiClient;
final _token = 'token';
NetworkDataRepository(this._apiClient);
@override
2025-05-19 16:06:51 +03:00
Future<String> getData() async =>
await _apiClient.sendRequest(
url: 'www.google.com',
token: _token,
requestBody: {'type': 'data'},
);
2021-04-23 08:43:10 +03:00
}
abstract class ApiClient {
2025-05-19 16:06:51 +03:00
Future sendRequest({@required String? url, String? token, Map? requestBody});
2021-04-23 08:43:10 +03:00
}
class ApiClientMock implements ApiClient {
@override
Future sendRequest(
{@required String? url, String? token, Map? requestBody}) async {
return 'Local Data';
}
}
class ApiClientImpl implements ApiClient {
@override
Future sendRequest(
{@required String? url, String? token, Map? requestBody}) async {
return 'Network data';
}
}
2025-05-02 11:41:18 +03:00
```
## Logging
CherryPick supports centralized logging of all dependency injection (DI) events and errors. You can globally enable logs for your application or test environment with:
```dart
import 'package:cherrypick/cherrypick.dart';
void main() {
// Set a global logger before any scopes are created
CherryPick.setGlobalLogger(PrintLogger()); // or your custom logger
final scope = CherryPick.openRootScope();
// All DI actions and errors will now be logged!
}
```
- All dependency resolution, scope creation, module installation, and circular dependency errors will be sent to your logger (via info/error method).
- By default, logs are off (SilentLogger is used in production).
If you want fine-grained, test-local, or isolated logging, you can provide a logger directly to each scope:
```dart
final logger = MockLogger();
final scope = Scope(null, logger: logger); // works in tests for isolation
scope.installModules([...]);
```
2025-05-02 11:46:47 +03:00
## Features
2025-05-19 16:06:51 +03:00
- [x] Main Scope and Named Subscopes
- [x] Named Instance Binding and Resolution
- [x] Asynchronous and Synchronous Providers
- [x] Providers Supporting Runtime Parameters
- [x] Singleton Lifecycle Management
- [x] Modular and Hierarchical Composition
- [x] Null-safe Resolution (tryResolve/tryResolveAsync)
- [x] Circular Dependency Detection (Local and Global)
- [x] Comprehensive logging of dependency injection state and actions
## Quick Guide: Circular Dependency Detection
CherryPick can detect circular dependencies in your DI configuration, helping you avoid infinite loops and hard-to-debug errors.
**How to use:**
### 1. Enable Cycle Detection for Development
**Local detection (within one scope):**
```dart
final scope = CherryPick.openSafeRootScope(); // Local detection enabled by default
// or, for an existing scope:
scope.enableCycleDetection();
```
**Global detection (across all scopes):**
```dart
CherryPick.enableGlobalCrossScopeCycleDetection();
final rootScope = CherryPick.openGlobalSafeRootScope();
```
### 2. Error Example
If you declare mutually dependent services:
```dart
class A { A(B b); }
class B { B(A a); }
scope.installModules([
Module((bind) {
bind<A>().to((s) => A(s.resolve<B>()));
bind<B>().to((s) => B(s.resolve<A>()));
}),
]);
scope.resolve<A>(); // Throws CircularDependencyException
```
### 3. Typical Usage Pattern
- **Always enable detection** in debug and test environments for maximum safety.
- **Disable detection** in production for performance (after code is tested).
```dart
import 'package:flutter/foundation.dart';
void main() {
if (kDebugMode) {
CherryPick.enableGlobalCycleDetection();
CherryPick.enableGlobalCrossScopeCycleDetection();
}
runApp(MyApp());
}
```
### 4. Handling and Debugging Errors
On detection, `CircularDependencyException` is thrown with a readable dependency chain:
```dart
try {
scope.resolve<MyService>();
} on CircularDependencyException catch (e) {
print('Dependency chain: ${e.dependencyChain}');
}
```
**More details:** See [cycle_detection.en.md](doc/cycle_detection.en.md)
## Documentation
- [Circular Dependency Detection (English)](doc/cycle_detection.en.md)
- [Обнаружение циклических зависимостей (Русский)](doc/cycle_detection.ru.md)
2025-05-02 11:46:47 +03:00
## Contributing
2025-05-19 16:06:51 +03:00
Contributions are welcome! Please open issues or submit pull requests on [GitHub](https://github.com/pese-git/cherrypick).
2025-05-02 11:41:18 +03:00
2025-05-02 11:46:47 +03:00
## License
2025-05-02 11:41:18 +03:00
2025-08-08 23:24:05 +03:00
Licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
2025-05-19 16:06:51 +03:00
---
2025-05-02 11:41:18 +03:00
2025-05-02 11:46:47 +03:00
**Important:** Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for specific language governing permissions and limitations under the License.
2025-05-02 11:41:18 +03:00
2025-05-02 11:46:47 +03:00
## Links
2025-05-02 11:41:18 +03:00
- [GitHub Repository](https://github.com/pese-git/cherrypick)