mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-23 21:13:35 +00:00
docs+feat: add Disposable interface source and usage example
feat(core,doc): unified async dispose mechanism for resource cleanup BREAKING CHANGE: - Added full support for asynchronous resource cleanup via a unified FutureOr<void> dispose() method in the Disposable interface. - The Scope now provides only Future<void> dispose() for disposing all tracked resources and child scopes (sync-only dispose() was removed). - All calls to cleanup in code and tests (scope itself, subscopes, and custom modules) now require await ...dispose(). - Documentation and all examples updated: resource management is always async and must be awaited; Disposable implementers may use both sync and async cleanup. - Old-style, synchronous cleanup methods have been completely removed (API is now consistently async for all DI lifecycle management). - Example and tutorial code now demonstrate async resource disposal patterns.
This commit is contained in:
@@ -47,7 +47,7 @@ packages:
|
||||
path: "../cherrypick"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0-dev.3"
|
||||
version: "3.0.0-dev.5"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -79,8 +79,55 @@ final str = rootScope.resolve<String>();
|
||||
// Resolve a dependency asynchronously
|
||||
final result = await rootScope.resolveAsync<String>();
|
||||
|
||||
// Close the root scope once done
|
||||
CherryPick.closeRootScope();
|
||||
// Recommended: Close the root scope and release all resources
|
||||
await CherryPick.closeRootScope();
|
||||
|
||||
// Alternatively, you may manually call dispose on any scope you manage individually
|
||||
// await rootScope.dispose();
|
||||
```
|
||||
|
||||
### Automatic resource management (`Disposable`, `dispose`)
|
||||
|
||||
CherryPick automatically manages the lifecycle of any object registered via DI that implements the `Disposable` interface.
|
||||
|
||||
**Best practice:**
|
||||
Always finish your work with `await CherryPick.closeRootScope()` (for the root scope) or `await scope.closeSubScope('key')` (for subscopes).
|
||||
These methods will automatically await `dispose()` on all resolved objects (e.g., singletons) that implement `Disposable`, ensuring proper and complete resource cleanup—sync or async.
|
||||
|
||||
Manual `await scope.dispose()` may be useful if you manually manage custom scopes.
|
||||
|
||||
#### Example
|
||||
|
||||
```dart
|
||||
class MyService implements Disposable {
|
||||
@override
|
||||
FutureOr<void> dispose() async {
|
||||
// release resources, close streams, perform async shutdown, etc.
|
||||
print('MyService disposed!');
|
||||
}
|
||||
}
|
||||
|
||||
final scope = openRootScope();
|
||||
scope.installModules([
|
||||
ModuleImpl(),
|
||||
]);
|
||||
|
||||
final service = scope.resolve<MyService>();
|
||||
|
||||
// ... use service
|
||||
|
||||
// Recommended completion:
|
||||
await CherryPick.closeRootScope(); // will print: MyService disposed!
|
||||
|
||||
// Or, to close and clean up a subscope and its resources:
|
||||
await scope.closeSubScope('feature');
|
||||
|
||||
class ModuleImpl extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<MyService>().toProvide(() => MyService()).singleton();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Working with Subscopes
|
||||
|
||||
40
cherrypick/example/disposable_example.dart
Normal file
40
cherrypick/example/disposable_example.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
|
||||
/// Ваш сервис с освобождением ресурсов
|
||||
class MyService implements Disposable {
|
||||
bool wasDisposed = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Например: закрыть соединение, остановить таймер, освободить память
|
||||
wasDisposed = true;
|
||||
print('MyService disposed!');
|
||||
}
|
||||
|
||||
void doSomething() => print('Doing something...');
|
||||
}
|
||||
|
||||
void main() {
|
||||
final scope = CherryPick.openRootScope();
|
||||
|
||||
// Регистрируем биндинг (singleton для примера)
|
||||
scope.installModules([
|
||||
ModuleImpl(),
|
||||
]);
|
||||
|
||||
// Получаем зависимость
|
||||
final service = scope.resolve<MyService>();
|
||||
service.doSomething(); // «Doing something...»
|
||||
|
||||
// Освобождаем все ресурсы
|
||||
scope.dispose();
|
||||
print('Service wasDisposed = ${service.wasDisposed}'); // true
|
||||
}
|
||||
|
||||
/// Пример модуля CherryPick
|
||||
class ModuleImpl extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<MyService>().toProvide(() => MyService()).singleton();
|
||||
}
|
||||
}
|
||||
@@ -21,3 +21,4 @@ export 'package:cherrypick/src/helper.dart';
|
||||
export 'package:cherrypick/src/module.dart';
|
||||
export 'package:cherrypick/src/scope.dart';
|
||||
export 'package:cherrypick/src/logger.dart';
|
||||
export 'package:cherrypick/src/disposable.dart';
|
||||
|
||||
10
cherrypick/lib/src/disposable.dart
Normal file
10
cherrypick/lib/src/disposable.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
/// Базовый интерфейс для автоматического управления ресурсами в CherryPick.
|
||||
/// Если объект реализует [Disposable], DI-контейнер вызовет [dispose] при очистке scope.
|
||||
import 'dart:async';
|
||||
|
||||
/// Interface for resources that need to be disposed synchronously or asynchronously.
|
||||
abstract class Disposable {
|
||||
/// Releases all resources held by this object.
|
||||
/// For sync disposables, just implement as void; for async ones, return Future.
|
||||
FutureOr<void> dispose();
|
||||
}
|
||||
@@ -95,8 +95,11 @@ class CherryPick {
|
||||
/// CherryPick.closeRootScope();
|
||||
/// ```
|
||||
static void closeRootScope() {
|
||||
if (_rootScope != null) {
|
||||
_rootScope!.dispose(); // Автоматический вызов dispose для rootScope!
|
||||
_rootScope = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Globally enables cycle detection for all new [Scope]s created by CherryPick.
|
||||
///
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'dart:collection';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cherrypick/src/cycle_detector.dart';
|
||||
import 'package:cherrypick/src/disposable.dart';
|
||||
import 'package:cherrypick/src/global_cycle_detector.dart';
|
||||
import 'package:cherrypick/src/binding_resolver.dart';
|
||||
import 'package:cherrypick/src/module.dart';
|
||||
@@ -28,6 +29,9 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
@override
|
||||
CherryPickLogger get logger => _logger;
|
||||
|
||||
/// COLLECTS all resolved instances that implement [Disposable].
|
||||
final Set<Disposable> _disposables = HashSet();
|
||||
|
||||
/// RU: Метод возвращает родительский [Scope].
|
||||
///
|
||||
/// ENG: The method returns the parent [Scope].
|
||||
@@ -94,14 +98,15 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
return _scopeMap[name]!;
|
||||
}
|
||||
|
||||
/// RU: Метод закрывает дочерний (дополнительный) [Scope].
|
||||
/// RU: Метод закрывает дочерний (дополнительный) [Scope] асинхронно.
|
||||
///
|
||||
/// ENG: The method closes child (additional) [Scope].
|
||||
/// ENG: The method closes child (additional) [Scope] asynchronously.
|
||||
///
|
||||
/// return [Scope]
|
||||
void closeSubScope(String name) {
|
||||
/// return [Future<void>]
|
||||
Future<void> closeSubScope(String name) async {
|
||||
final childScope = _scopeMap[name];
|
||||
if (childScope != null) {
|
||||
await childScope.dispose(); // асинхронный вызов
|
||||
// Очищаем детектор для дочернего скоупа
|
||||
if (childScope.scopeId != null) {
|
||||
GlobalCycleDetector.instance.removeScopeDetector(childScope.scopeId!);
|
||||
@@ -175,9 +180,10 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
///
|
||||
T resolve<T>({String? named, dynamic params}) {
|
||||
// Используем глобальное отслеживание, если включено
|
||||
T result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
try {
|
||||
return withGlobalCycleDetection<T>(T, named, () {
|
||||
result = withGlobalCycleDetection<T>(T, named, () {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
} catch (e, s) {
|
||||
@@ -195,7 +201,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
result = _resolveWithLocalDetection<T>(named: named, params: params);
|
||||
} catch (e, s) {
|
||||
logger.error(
|
||||
formatLogMessage(
|
||||
@@ -210,6 +216,8 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
_trackDisposable(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// RU: Разрешение с локальным детектором циклических зависимостей.
|
||||
@@ -251,13 +259,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
///
|
||||
T? tryResolve<T>({String? named, dynamic params}) {
|
||||
// Используем глобальное отслеживание, если включено
|
||||
T? result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
return withGlobalCycleDetection<T?>(T, named, () {
|
||||
result = withGlobalCycleDetection<T?>(T, named, () {
|
||||
return _tryResolveWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
} else {
|
||||
return _tryResolveWithLocalDetection<T>(named: named, params: params);
|
||||
result = _tryResolveWithLocalDetection<T>(named: named, params: params);
|
||||
}
|
||||
if (result != null) _trackDisposable(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// RU: Попытка разрешения с локальным детектором циклических зависимостей.
|
||||
@@ -295,13 +306,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
///
|
||||
Future<T> resolveAsync<T>({String? named, dynamic params}) async {
|
||||
// Используем глобальное отслеживание, если включено
|
||||
T result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
return withGlobalCycleDetection<Future<T>>(T, named, () async {
|
||||
result = await withGlobalCycleDetection<Future<T>>(T, named, () async {
|
||||
return await _resolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
} else {
|
||||
return await _resolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
result = await _resolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
}
|
||||
_trackDisposable(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// RU: Асинхронное разрешение с локальным детектором циклических зависимостей.
|
||||
@@ -320,13 +334,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
|
||||
Future<T?> tryResolveAsync<T>({String? named, dynamic params}) async {
|
||||
// Используем глобальное отслеживание, если включено
|
||||
T? result;
|
||||
if (isGlobalCycleDetectionEnabled) {
|
||||
return withGlobalCycleDetection<Future<T?>>(T, named, () async {
|
||||
result = await withGlobalCycleDetection<Future<T?>>(T, named, () async {
|
||||
return await _tryResolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
});
|
||||
} else {
|
||||
return await _tryResolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
result = await _tryResolveAsyncWithLocalDetection<T>(named: named, params: params);
|
||||
}
|
||||
if (result != null) _trackDisposable(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// RU: Асинхронная попытка разрешения с локальным детектором циклических зависимостей.
|
||||
@@ -366,4 +383,27 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// INTERNAL: Tracks Disposable objects
|
||||
void _trackDisposable(Object? obj) {
|
||||
if (obj is Disposable && !_disposables.contains(obj)) {
|
||||
_disposables.add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls dispose on all tracked disposables and child scopes recursively (async).
|
||||
Future<void> dispose() async {
|
||||
// First dispose children scopes
|
||||
for (final subScope in _scopeMap.values) {
|
||||
await subScope.dispose();
|
||||
}
|
||||
_scopeMap.clear();
|
||||
// Then dispose own disposables
|
||||
for (final d in _disposables) {
|
||||
try {
|
||||
await d.dispose();
|
||||
} catch (_) {}
|
||||
}
|
||||
_disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,110 @@
|
||||
import 'package:cherrypick/cherrypick.dart';
|
||||
import 'package:cherrypick/cherrypick.dart' show Disposable, Module, Scope, CherryPick;
|
||||
import 'dart:async';
|
||||
import 'package:test/test.dart';
|
||||
import '../mock_logger.dart';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Вспомогательные классы для тестов
|
||||
|
||||
class AsyncExampleDisposable implements Disposable {
|
||||
bool disposed = false;
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
class AsyncExampleModule extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<AsyncExampleDisposable>().toProvide(() => AsyncExampleDisposable()).singleton();
|
||||
}
|
||||
}
|
||||
|
||||
class TestDisposable implements Disposable {
|
||||
bool disposed = false;
|
||||
@override
|
||||
FutureOr<void> dispose() {
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
class AnotherDisposable implements Disposable {
|
||||
bool disposed = false;
|
||||
@override
|
||||
FutureOr<void> dispose() {
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
class CountingDisposable implements Disposable {
|
||||
int disposeCount = 0;
|
||||
@override
|
||||
FutureOr<void> dispose() {
|
||||
disposeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
class ModuleCountingDisposable extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<CountingDisposable>().toProvide(() => CountingDisposable()).singleton();
|
||||
}
|
||||
}
|
||||
|
||||
class ModuleWithDisposable extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<TestDisposable>().toProvide(() => TestDisposable()).singleton();
|
||||
bind<AnotherDisposable>().toProvide(() => AnotherDisposable()).singleton();
|
||||
bind<String>().toProvide(() => 'super string').singleton();
|
||||
}
|
||||
}
|
||||
|
||||
class TestModule<T> extends Module {
|
||||
final T value;
|
||||
final String? name;
|
||||
TestModule({required this.value, this.name});
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
if (name == null) {
|
||||
bind<T>().toInstance(value);
|
||||
} else {
|
||||
bind<T>().withName(name!).toInstance(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _InlineModule extends Module {
|
||||
final void Function(Module, Scope) _builder;
|
||||
_InlineModule(this._builder);
|
||||
@override
|
||||
void builder(Scope s) => _builder(this, s);
|
||||
}
|
||||
|
||||
class AsyncCreatedDisposable implements Disposable {
|
||||
bool disposed = false;
|
||||
@override
|
||||
void dispose() {
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
class AsyncModule extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<AsyncCreatedDisposable>()
|
||||
.toProvideAsync(() async {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
return AsyncCreatedDisposable();
|
||||
})
|
||||
.singleton();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void main() {
|
||||
// --------------------------------------------------------------------------
|
||||
group('Scope & Subscope Management', () {
|
||||
@@ -10,12 +113,20 @@ void main() {
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(scope.parentScope, null);
|
||||
});
|
||||
|
||||
test('Can open and retrieve the same subScope by key', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(Scope(scope, logger: logger), isNotNull); // эквивалент
|
||||
});
|
||||
test('closeSubScope removes subscope so next openSubScope returns new', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
final subScope = scope.openSubScope("child");
|
||||
expect(scope.openSubScope("child"), same(subScope));
|
||||
await scope.closeSubScope("child");
|
||||
final newSubScope = scope.openSubScope("child");
|
||||
expect(newSubScope, isNot(same(subScope)));
|
||||
});
|
||||
|
||||
test('closeSubScope removes subscope so next openSubScope returns new', () {
|
||||
final logger = MockLogger();
|
||||
@@ -32,7 +143,6 @@ void main() {
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
|
||||
test('Resolves value after adding a dependency', () {
|
||||
final logger = MockLogger();
|
||||
final expectedValue = 'test string';
|
||||
@@ -40,7 +150,6 @@ void main() {
|
||||
.installModules([TestModule<String>(value: expectedValue)]);
|
||||
expect(scope.resolve<String>(), expectedValue);
|
||||
});
|
||||
|
||||
test('Returns a value from parent scope', () {
|
||||
final logger = MockLogger();
|
||||
final expectedValue = 5;
|
||||
@@ -48,10 +157,8 @@ void main() {
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
|
||||
parentScope.installModules([TestModule<int>(value: expectedValue)]);
|
||||
|
||||
expect(scope.resolve<int>(), expectedValue);
|
||||
});
|
||||
|
||||
test('Returns several values from parent container', () {
|
||||
final logger = MockLogger();
|
||||
final expectedIntValue = 5;
|
||||
@@ -65,14 +172,12 @@ void main() {
|
||||
expect(scope.resolve<int>(), expectedIntValue);
|
||||
expect(scope.resolve<String>(), expectedStringValue);
|
||||
});
|
||||
|
||||
test("Throws StateError if parent hasn't value too", () {
|
||||
final logger = MockLogger();
|
||||
final parentScope = Scope(null, logger: logger);
|
||||
final scope = Scope(parentScope, logger: logger);
|
||||
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
|
||||
test("After dropModules resolves fail", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)..installModules([TestModule<int>(value: 5)]);
|
||||
@@ -94,7 +199,6 @@ void main() {
|
||||
expect(scope.resolve<String>(named: "special"), "second");
|
||||
expect(scope.resolve<String>(), "first");
|
||||
});
|
||||
|
||||
test('Named binding does not clash with unnamed', () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
@@ -104,7 +208,6 @@ void main() {
|
||||
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
|
||||
expect(scope.resolve<String>(named: "bar"), "foo");
|
||||
});
|
||||
|
||||
test("tryResolve returns null for missing named", () {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
@@ -142,7 +245,6 @@ void main() {
|
||||
]);
|
||||
expect(await scope.resolveAsync<String>(), "async value");
|
||||
});
|
||||
|
||||
test('Resolve async provider', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
@@ -153,7 +255,6 @@ void main() {
|
||||
]);
|
||||
expect(await scope.resolveAsync<int>(), 7);
|
||||
});
|
||||
|
||||
test('Resolve async provider with param', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger)
|
||||
@@ -165,7 +266,6 @@ void main() {
|
||||
expect(await scope.resolveAsync<int>(params: 2), 6);
|
||||
expect(() => scope.resolveAsync<int>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
|
||||
test('tryResolveAsync returns null for missing', () async {
|
||||
final logger = MockLogger();
|
||||
final scope = Scope(null, logger: logger);
|
||||
@@ -181,42 +281,86 @@ void main() {
|
||||
final scope = Scope(null, logger: logger);
|
||||
expect(scope.tryResolve<int>(), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
// Не реализован:
|
||||
// test("Container bind() throws state error (if it's parent already has a resolver)", () {
|
||||
// final parentScope = new Scope(null).installModules([TestModule<String>(value: "string one")]);
|
||||
// final scope = new Scope(parentScope);
|
||||
// --------------------------------------------------------------------------
|
||||
group('Disposable resource management', () {
|
||||
test('scope.disposeAsync calls dispose on singleton disposable', () async {
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.installModules([ModuleWithDisposable()]);
|
||||
final t = scope.resolve<TestDisposable>();
|
||||
expect(t.disposed, isFalse);
|
||||
await scope.dispose();
|
||||
expect(t.disposed, isTrue);
|
||||
});
|
||||
test('scope.disposeAsync calls dispose on all unique disposables', () async {
|
||||
final scope = Scope(null, logger: MockLogger());
|
||||
scope.installModules([ModuleWithDisposable()]);
|
||||
final t1 = scope.resolve<TestDisposable>();
|
||||
final t2 = scope.resolve<AnotherDisposable>();
|
||||
expect(t1.disposed, isFalse);
|
||||
expect(t2.disposed, isFalse);
|
||||
await scope.dispose();
|
||||
expect(t1.disposed, isTrue);
|
||||
expect(t2.disposed, isTrue);
|
||||
});
|
||||
test('calling disposeAsync twice does not throw and not call twice', () async {
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.installModules([ModuleWithDisposable()]);
|
||||
final t = scope.resolve<TestDisposable>();
|
||||
await scope.dispose();
|
||||
await scope.dispose();
|
||||
expect(t.disposed, isTrue);
|
||||
});
|
||||
test('Non-disposable dependency is ignored by scope.disposeAsync', () async {
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.installModules([ModuleWithDisposable()]);
|
||||
final s = scope.resolve<String>();
|
||||
expect(s, 'super string');
|
||||
await scope.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
// expect(
|
||||
// () => scope.installModules([TestModule<String>(value: "string two")]),
|
||||
// throwsA(isA<StateError>()));
|
||||
// });
|
||||
// --------------------------------------------------------------------------
|
||||
// Расширенные edge-тесты для dispose и subScope
|
||||
group('Scope/subScope dispose edge cases', () {
|
||||
test('Dispose called in closed subScope only', () async {
|
||||
final root = CherryPick.openRootScope();
|
||||
final sub = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]);
|
||||
final d = sub.resolve<CountingDisposable>();
|
||||
expect(d.disposeCount, 0);
|
||||
|
||||
await root.closeSubScope('feature');
|
||||
expect(d.disposeCount, 1); // dispose должен быть вызван
|
||||
|
||||
// Повторное закрытие не вызывает double-dispose
|
||||
await root.closeSubScope('feature');
|
||||
expect(d.disposeCount, 1);
|
||||
|
||||
// Повторное открытие subScope создает NEW instance (dispose на старый не вызовется снова)
|
||||
final sub2 = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]);
|
||||
final d2 = sub2.resolve<CountingDisposable>();
|
||||
expect(identical(d, d2), isFalse);
|
||||
await root.closeSubScope('feature');
|
||||
expect(d2.disposeCount, 1);
|
||||
});
|
||||
test('Dispose for all nested subScopes on root disposeAsync', () async {
|
||||
final root = CherryPick.openRootScope();
|
||||
root.openSubScope('a').openSubScope('b').installModules([ModuleCountingDisposable()]);
|
||||
final d = root.openSubScope('a').openSubScope('b').resolve<CountingDisposable>();
|
||||
await root.dispose();
|
||||
expect(d.disposeCount, 1);
|
||||
});
|
||||
});
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
group('Async disposable (Future test)', () {
|
||||
test('Async Disposable is awaited on disposeAsync', () async {
|
||||
final scope = CherryPick.openRootScope()..installModules([AsyncExampleModule()]);
|
||||
final d = scope.resolve<AsyncExampleDisposable>();
|
||||
expect(d.disposed, false);
|
||||
await scope.dispose();
|
||||
expect(d.disposed, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Вспомогательные модули
|
||||
|
||||
class TestModule<T> extends Module {
|
||||
final T value;
|
||||
final String? name;
|
||||
|
||||
TestModule({required this.value, this.name});
|
||||
@override
|
||||
void builder(Scope currentScope) {
|
||||
if (name == null) {
|
||||
bind<T>().toInstance(value);
|
||||
} else {
|
||||
bind<T>().withName(name!).toInstance(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Вспомогательный модуль для подстановки builder'а через конструктор
|
||||
class _InlineModule extends Module {
|
||||
final void Function(Module, Scope) _builder;
|
||||
_InlineModule(this._builder);
|
||||
|
||||
@override
|
||||
void builder(Scope s) => _builder(this, s);
|
||||
}
|
||||
|
||||
@@ -185,6 +185,41 @@ final service = scope.tryResolve<OptionalService>(); // returns null if not exis
|
||||
---
|
||||
|
||||
|
||||
## Automatic resource management: Disposable and dispose
|
||||
|
||||
CherryPick makes it easy to clean up resources for your singleton services and other objects registered in DI.
|
||||
If your class implements the `Disposable` interface, always **await** `scope.dispose()` (or `CherryPick.closeRootScope()`) when you want to free all resources in your scope — CherryPick will automatically await `dispose()` for every object that implements `Disposable` and was resolved via DI.
|
||||
This ensures safe and graceful resource management (including any async resource cleanup: streams, DB connections, sockets, etc.).
|
||||
|
||||
### Example
|
||||
|
||||
```dart
|
||||
class LoggingService implements Disposable {
|
||||
@override
|
||||
FutureOr<void> dispose() async {
|
||||
// Close files, streams, and perform async cleanup here.
|
||||
print('LoggingService disposed!');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
final scope = openRootScope();
|
||||
scope.installModules([
|
||||
_LoggingModule(),
|
||||
]);
|
||||
final logger = scope.resolve<LoggingService>();
|
||||
// Use logger...
|
||||
await scope.dispose(); // prints: LoggingService disposed!
|
||||
}
|
||||
|
||||
class _LoggingModule extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<LoggingService>().toProvide(() => LoggingService()).singleton();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency injection with annotations & code generation
|
||||
|
||||
CherryPick supports DI with annotations, letting you eliminate manual DI setup.
|
||||
|
||||
@@ -185,6 +185,41 @@ final service = scope.tryResolve<OptionalService>(); // вернет null, ес
|
||||
|
||||
---
|
||||
|
||||
## Автоматическое управление ресурсами: Disposable и dispose
|
||||
|
||||
CherryPick позволяет автоматически очищать ресурсы для ваших синглтонов и любых сервисов, зарегистрированных через DI.
|
||||
Если ваш класс реализует интерфейс `Disposable`, всегда вызывайте и **await**-те `scope.dispose()` (или `CherryPick.closeRootScope()`), когда хотите освободить все ресурсы — CherryPick дождётся завершения `dispose()` для всех объектов, которые реализуют Disposable и были резолвлены из DI.
|
||||
Это позволяет избежать утечек памяти, корректно завершать процессы и грамотно освобождать любые ресурсы (файлы, потоки, соединения и т.д., включая async).
|
||||
|
||||
### Пример
|
||||
|
||||
```dart
|
||||
class LoggingService implements Disposable {
|
||||
@override
|
||||
FutureOr<void> dispose() async {
|
||||
// Закрыть файлы, потоки, соединения и т.д. (можно с await)
|
||||
print('LoggingService disposed!');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> main() async {
|
||||
final scope = openRootScope();
|
||||
scope.installModules([
|
||||
_LoggingModule(),
|
||||
]);
|
||||
final logger = scope.resolve<LoggingService>();
|
||||
// Используем logger...
|
||||
await scope.dispose(); // выведет: LoggingService disposed!
|
||||
}
|
||||
|
||||
class _LoggingModule extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<LoggingService>().toProvide(() => LoggingService()).singleton();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Внедрение зависимостей через аннотации и автогенерацию
|
||||
|
||||
CherryPick поддерживает DI через аннотации, что позволяет полностью избавиться от ручного внедрения зависимостей.
|
||||
|
||||
@@ -75,8 +75,54 @@ Example:
|
||||
// or
|
||||
final str = rootScope.tryResolve<String>();
|
||||
|
||||
// close main scope
|
||||
Cherrypick.closeRootScope();
|
||||
// Recommended: Close the root scope & automatically release all Disposable resources
|
||||
await Cherrypick.closeRootScope();
|
||||
// Or, for advanced/manual scenarios:
|
||||
// await rootScope.dispose();
|
||||
```
|
||||
|
||||
### Automatic resource management (`Disposable`, `dispose`)
|
||||
|
||||
If your service implements the `Disposable` interface, CherryPick will automatically await `dispose()` when you close a scope.
|
||||
|
||||
**Best practice:**
|
||||
Always finish your work with `await Cherrypick.closeRootScope()` (for the root scope) or `await scope.closeSubScope('feature')` (for subscopes).
|
||||
These methods will automatically await `dispose()` on all resolved objects implementing `Disposable`, ensuring safe and complete cleanup (sync and async).
|
||||
|
||||
Manual `await scope.dispose()` is available if you manage scopes yourself.
|
||||
|
||||
#### Example
|
||||
|
||||
```dart
|
||||
class MyService implements Disposable {
|
||||
@override
|
||||
FutureOr<void> dispose() async {
|
||||
// release resources, close connections, perform async shutdown, etc.
|
||||
print('MyService disposed!');
|
||||
}
|
||||
}
|
||||
|
||||
final scope = openRootScope();
|
||||
scope.installModules([
|
||||
ModuleImpl(),
|
||||
]);
|
||||
|
||||
final service = scope.resolve<MyService>();
|
||||
|
||||
// ... use service
|
||||
|
||||
// Recommended:
|
||||
await Cherrypick.closeRootScope(); // will print: MyService disposed!
|
||||
|
||||
// Or, to close a subscope:
|
||||
await scope.closeSubScope('feature');
|
||||
|
||||
class ModuleImpl extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<MyService>().toProvide(() => MyService()).singleton();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
@@ -75,8 +75,54 @@ Scope - это контейнер, который хранит все дерев
|
||||
// или
|
||||
final str = rootScope.tryResolve<String>();
|
||||
|
||||
// закрыть главный scope
|
||||
Cherrypick.closeRootScope();
|
||||
// Рекомендуется: закрывайте главный scope для автоматического освобождения всех ресурсов
|
||||
await Cherrypick.closeRootScope();
|
||||
// Или, для продвинутых/ручных сценариев:
|
||||
// await rootScope.dispose();
|
||||
```
|
||||
|
||||
### Автоматическое управление ресурсами (`Disposable`, `dispose`)
|
||||
|
||||
Если ваш сервис реализует интерфейс `Disposable`, CherryPick автоматически дождётся выполнения `dispose()` при закрытии scope.
|
||||
|
||||
**Рекомендация:**
|
||||
Завершайте работу через `await Cherrypick.closeRootScope()` (для root scope) или `await scope.closeSubScope('feature')` (для подскоупов).
|
||||
Эти методы автоматически await-ят `dispose()` для всех разрешённых через DI объектов, реализующих `Disposable`, обеспечивая корректную очистку (sync и async) и высвобождение ресурсов.
|
||||
|
||||
Вызывайте `await scope.dispose()` если вы явно управляете custom-скоупом.
|
||||
|
||||
#### Пример
|
||||
|
||||
```dart
|
||||
class MyService implements Disposable {
|
||||
@override
|
||||
FutureOr<void> dispose() async {
|
||||
// закрытие ресурса, соединений, таймеров и т.п., async/await
|
||||
print('MyService disposed!');
|
||||
}
|
||||
}
|
||||
|
||||
final scope = openRootScope();
|
||||
scope.installModules([
|
||||
ModuleImpl(),
|
||||
]);
|
||||
|
||||
final service = scope.resolve<MyService>();
|
||||
|
||||
// ... используем сервис ...
|
||||
|
||||
// Рекомендуемый финал:
|
||||
await Cherrypick.closeRootScope(); // выведет в консоль 'MyService disposed!'
|
||||
|
||||
// Или для подскоупа:
|
||||
await scope.closeSubScope('feature');
|
||||
|
||||
class ModuleImpl extends Module {
|
||||
@override
|
||||
void builder(Scope scope) {
|
||||
bind<MyService>().toProvide(() => MyService()).singleton();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Логирование
|
||||
|
||||
@@ -127,7 +127,7 @@ packages:
|
||||
path: "../../cherrypick"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0-dev.3"
|
||||
version: "3.0.0-dev.5"
|
||||
cherrypick_annotations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -141,7 +141,7 @@ packages:
|
||||
path: "../../cherrypick_flutter"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.1.3-dev.3"
|
||||
version: "1.1.3-dev.5"
|
||||
cherrypick_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
||||
@@ -151,7 +151,7 @@ packages:
|
||||
path: "../../cherrypick"
|
||||
relative: true
|
||||
source: path
|
||||
version: "3.0.0-dev.3"
|
||||
version: "3.0.0-dev.5"
|
||||
cherrypick_annotations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user