From a9c95f6a898f1b72ff5c03e2ed8921f77aeba3fb Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Fri, 1 Aug 2025 15:07:12 +0300 Subject: [PATCH 1/4] 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 dispose() method in the Disposable interface. - The Scope now provides only Future 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. --- benchmark_di/pubspec.lock | 2 +- cherrypick/README.md | 51 ++++- cherrypick/example/disposable_example.dart | 40 ++++ cherrypick/lib/cherrypick.dart | 1 + cherrypick/lib/src/disposable.dart | 10 + cherrypick/lib/src/helper.dart | 5 +- cherrypick/lib/src/scope.dart | 64 ++++-- cherrypick/test/src/scope_test.dart | 236 +++++++++++++++++---- doc/full_tutorial_en.md | 35 +++ doc/full_tutorial_ru.md | 35 +++ doc/quick_start_en.md | 50 ++++- doc/quick_start_ru.md | 50 ++++- examples/client_app/pubspec.lock | 4 +- examples/postly/pubspec.lock | 2 +- 14 files changed, 516 insertions(+), 69 deletions(-) create mode 100644 cherrypick/example/disposable_example.dart create mode 100644 cherrypick/lib/src/disposable.dart diff --git a/benchmark_di/pubspec.lock b/benchmark_di/pubspec.lock index 5f3e929..05cb540 100644 --- a/benchmark_di/pubspec.lock +++ b/benchmark_di/pubspec.lock @@ -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: diff --git a/cherrypick/README.md b/cherrypick/README.md index 95a839d..24664e9 100644 --- a/cherrypick/README.md +++ b/cherrypick/README.md @@ -79,8 +79,55 @@ final str = rootScope.resolve(); // Resolve a dependency asynchronously final result = await rootScope.resolveAsync(); -// 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 dispose() async { + // release resources, close streams, perform async shutdown, etc. + print('MyService disposed!'); + } +} + +final scope = openRootScope(); +scope.installModules([ + ModuleImpl(), +]); + +final service = scope.resolve(); + +// ... 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().toProvide(() => MyService()).singleton(); + } +} ``` #### Working with Subscopes diff --git a/cherrypick/example/disposable_example.dart b/cherrypick/example/disposable_example.dart new file mode 100644 index 0000000..722e339 --- /dev/null +++ b/cherrypick/example/disposable_example.dart @@ -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(); + service.doSomething(); // «Doing something...» + + // Освобождаем все ресурсы + scope.dispose(); + print('Service wasDisposed = ${service.wasDisposed}'); // true +} + +/// Пример модуля CherryPick +class ModuleImpl extends Module { + @override + void builder(Scope scope) { + bind().toProvide(() => MyService()).singleton(); + } +} diff --git a/cherrypick/lib/cherrypick.dart b/cherrypick/lib/cherrypick.dart index 0ab1080..31380e7 100644 --- a/cherrypick/lib/cherrypick.dart +++ b/cherrypick/lib/cherrypick.dart @@ -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'; diff --git a/cherrypick/lib/src/disposable.dart b/cherrypick/lib/src/disposable.dart new file mode 100644 index 0000000..37d93ee --- /dev/null +++ b/cherrypick/lib/src/disposable.dart @@ -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 dispose(); +} diff --git a/cherrypick/lib/src/helper.dart b/cherrypick/lib/src/helper.dart index ea715c3..3795904 100644 --- a/cherrypick/lib/src/helper.dart +++ b/cherrypick/lib/src/helper.dart @@ -95,7 +95,10 @@ class CherryPick { /// CherryPick.closeRootScope(); /// ``` static void closeRootScope() { - _rootScope = null; + if (_rootScope != null) { + _rootScope!.dispose(); // Автоматический вызов dispose для rootScope! + _rootScope = null; + } } /// Globally enables cycle detection for all new [Scope]s created by CherryPick. diff --git a/cherrypick/lib/src/scope.dart b/cherrypick/lib/src/scope.dart index 13a7a67..fde7470 100644 --- a/cherrypick/lib/src/scope.dart +++ b/cherrypick/lib/src/scope.dart @@ -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 _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] + Future 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({String? named, dynamic params}) { // Используем глобальное отслеживание, если включено + T result; if (isGlobalCycleDetectionEnabled) { try { - return withGlobalCycleDetection(T, named, () { + result = withGlobalCycleDetection(T, named, () { return _resolveWithLocalDetection(named: named, params: params); }); } catch (e, s) { @@ -195,7 +201,7 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { } } else { try { - return _resolveWithLocalDetection(named: named, params: params); + result = _resolveWithLocalDetection(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({String? named, dynamic params}) { // Используем глобальное отслеживание, если включено + T? result; if (isGlobalCycleDetectionEnabled) { - return withGlobalCycleDetection(T, named, () { + result = withGlobalCycleDetection(T, named, () { return _tryResolveWithLocalDetection(named: named, params: params); }); } else { - return _tryResolveWithLocalDetection(named: named, params: params); + result = _tryResolveWithLocalDetection(named: named, params: params); } + if (result != null) _trackDisposable(result); + return result; } /// RU: Попытка разрешения с локальным детектором циклических зависимостей. @@ -295,13 +306,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { /// Future resolveAsync({String? named, dynamic params}) async { // Используем глобальное отслеживание, если включено + T result; if (isGlobalCycleDetectionEnabled) { - return withGlobalCycleDetection>(T, named, () async { + result = await withGlobalCycleDetection>(T, named, () async { return await _resolveAsyncWithLocalDetection(named: named, params: params); }); } else { - return await _resolveAsyncWithLocalDetection(named: named, params: params); + result = await _resolveAsyncWithLocalDetection(named: named, params: params); } + _trackDisposable(result); + return result; } /// RU: Асинхронное разрешение с локальным детектором циклических зависимостей. @@ -320,13 +334,16 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin { Future tryResolveAsync({String? named, dynamic params}) async { // Используем глобальное отслеживание, если включено + T? result; if (isGlobalCycleDetectionEnabled) { - return withGlobalCycleDetection>(T, named, () async { + result = await withGlobalCycleDetection>(T, named, () async { return await _tryResolveAsyncWithLocalDetection(named: named, params: params); }); } else { - return await _tryResolveAsyncWithLocalDetection(named: named, params: params); + result = await _tryResolveAsyncWithLocalDetection(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 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(); + } } diff --git a/cherrypick/test/src/scope_test.dart b/cherrypick/test/src/scope_test.dart index feec69b..edb35ea 100644 --- a/cherrypick/test/src/scope_test.dart +++ b/cherrypick/test/src/scope_test.dart @@ -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 dispose() async { + await Future.delayed(Duration(milliseconds: 10)); + disposed = true; + } +} + +class AsyncExampleModule extends Module { + @override + void builder(Scope scope) { + bind().toProvide(() => AsyncExampleDisposable()).singleton(); + } +} + +class TestDisposable implements Disposable { + bool disposed = false; + @override + FutureOr dispose() { + disposed = true; + } +} + +class AnotherDisposable implements Disposable { + bool disposed = false; + @override + FutureOr dispose() { + disposed = true; + } +} + +class CountingDisposable implements Disposable { + int disposeCount = 0; + @override + FutureOr dispose() { + disposeCount++; + } +} + +class ModuleCountingDisposable extends Module { + @override + void builder(Scope scope) { + bind().toProvide(() => CountingDisposable()).singleton(); + } +} + +class ModuleWithDisposable extends Module { + @override + void builder(Scope scope) { + bind().toProvide(() => TestDisposable()).singleton(); + bind().toProvide(() => AnotherDisposable()).singleton(); + bind().toProvide(() => 'super string').singleton(); + } +} + +class TestModule extends Module { + final T value; + final String? name; + TestModule({required this.value, this.name}); + @override + void builder(Scope currentScope) { + if (name == null) { + bind().toInstance(value); + } else { + bind().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() + .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(), throwsA(isA())); }); - test('Resolves value after adding a dependency', () { final logger = MockLogger(); final expectedValue = 'test string'; @@ -40,7 +150,6 @@ void main() { .installModules([TestModule(value: expectedValue)]); expect(scope.resolve(), 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(value: expectedValue)]); - expect(scope.resolve(), expectedValue); }); - test('Returns several values from parent container', () { final logger = MockLogger(); final expectedIntValue = 5; @@ -65,14 +172,12 @@ void main() { expect(scope.resolve(), expectedIntValue); expect(scope.resolve(), 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(), throwsA(isA())); }); - test("After dropModules resolves fail", () { final logger = MockLogger(); final scope = Scope(null, logger: logger)..installModules([TestModule(value: 5)]); @@ -94,7 +199,6 @@ void main() { expect(scope.resolve(named: "special"), "second"); expect(scope.resolve(), "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(), throwsA(isA())); expect(scope.resolve(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(), "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(), 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(params: 2), 6); expect(() => scope.resolveAsync(), throwsA(isA())); }); - 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(), isNull); }); - - // Не реализован: - // test("Container bind() throws state error (if it's parent already has a resolver)", () { - // final parentScope = new Scope(null).installModules([TestModule(value: "string one")]); - // final scope = new Scope(parentScope); - - // expect( - // () => scope.installModules([TestModule(value: "string two")]), - // throwsA(isA())); - // }); }); -} -// ---------------------------------------------------------------------------- -// Вспомогательные модули + // -------------------------------------------------------------------------- + group('Disposable resource management', () { + test('scope.disposeAsync calls dispose on singleton disposable', () async { + final scope = CherryPick.openRootScope(); + scope.installModules([ModuleWithDisposable()]); + final t = scope.resolve(); + 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(); + final t2 = scope.resolve(); + 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(); + 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(); + expect(s, 'super string'); + await scope.dispose(); + }); + }); -class TestModule extends Module { - final T value; - final String? name; + // -------------------------------------------------------------------------- + // Расширенные 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(); + expect(d.disposeCount, 0); - TestModule({required this.value, this.name}); - @override - void builder(Scope currentScope) { - if (name == null) { - bind().toInstance(value); - } else { - bind().withName(name!).toInstance(value); - } - } -} + await root.closeSubScope('feature'); + expect(d.disposeCount, 1); // dispose должен быть вызван -/// Вспомогательный модуль для подстановки builder'а через конструктор -class _InlineModule extends Module { - final void Function(Module, Scope) _builder; - _InlineModule(this._builder); + // Повторное закрытие не вызывает double-dispose + await root.closeSubScope('feature'); + expect(d.disposeCount, 1); - @override - void builder(Scope s) => _builder(this, s); -} + // Повторное открытие subScope создает NEW instance (dispose на старый не вызовется снова) + final sub2 = root.openSubScope('feature')..installModules([ModuleCountingDisposable()]); + final d2 = sub2.resolve(); + 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(); + 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(); + expect(d.disposed, false); + await scope.dispose(); + expect(d.disposed, true); + }); + }); +} \ No newline at end of file diff --git a/doc/full_tutorial_en.md b/doc/full_tutorial_en.md index 483164f..ac2fca4 100644 --- a/doc/full_tutorial_en.md +++ b/doc/full_tutorial_en.md @@ -185,6 +185,41 @@ final service = scope.tryResolve(); // 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 dispose() async { + // Close files, streams, and perform async cleanup here. + print('LoggingService disposed!'); + } +} + +Future main() async { + final scope = openRootScope(); + scope.installModules([ + _LoggingModule(), + ]); + final logger = scope.resolve(); + // Use logger... + await scope.dispose(); // prints: LoggingService disposed! +} + +class _LoggingModule extends Module { + @override + void builder(Scope scope) { + bind().toProvide(() => LoggingService()).singleton(); + } +} +``` + ## Dependency injection with annotations & code generation CherryPick supports DI with annotations, letting you eliminate manual DI setup. diff --git a/doc/full_tutorial_ru.md b/doc/full_tutorial_ru.md index 52f6c25..13c20f6 100644 --- a/doc/full_tutorial_ru.md +++ b/doc/full_tutorial_ru.md @@ -185,6 +185,41 @@ final service = scope.tryResolve(); // вернет null, ес --- +## Автоматическое управление ресурсами: Disposable и dispose + +CherryPick позволяет автоматически очищать ресурсы для ваших синглтонов и любых сервисов, зарегистрированных через DI. +Если ваш класс реализует интерфейс `Disposable`, всегда вызывайте и **await**-те `scope.dispose()` (или `CherryPick.closeRootScope()`), когда хотите освободить все ресурсы — CherryPick дождётся завершения `dispose()` для всех объектов, которые реализуют Disposable и были резолвлены из DI. +Это позволяет избежать утечек памяти, корректно завершать процессы и грамотно освобождать любые ресурсы (файлы, потоки, соединения и т.д., включая async). + +### Пример + +```dart +class LoggingService implements Disposable { + @override + FutureOr dispose() async { + // Закрыть файлы, потоки, соединения и т.д. (можно с await) + print('LoggingService disposed!'); + } +} + +Future main() async { + final scope = openRootScope(); + scope.installModules([ + _LoggingModule(), + ]); + final logger = scope.resolve(); + // Используем logger... + await scope.dispose(); // выведет: LoggingService disposed! +} + +class _LoggingModule extends Module { + @override + void builder(Scope scope) { + bind().toProvide(() => LoggingService()).singleton(); + } +} +``` + ## Внедрение зависимостей через аннотации и автогенерацию CherryPick поддерживает DI через аннотации, что позволяет полностью избавиться от ручного внедрения зависимостей. diff --git a/doc/quick_start_en.md b/doc/quick_start_en.md index 63ab9ae..48fe15c 100644 --- a/doc/quick_start_en.md +++ b/doc/quick_start_en.md @@ -75,8 +75,54 @@ Example: // or final str = rootScope.tryResolve(); - // 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 dispose() async { + // release resources, close connections, perform async shutdown, etc. + print('MyService disposed!'); + } +} + +final scope = openRootScope(); +scope.installModules([ + ModuleImpl(), +]); + +final service = scope.resolve(); + +// ... 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().toProvide(() => MyService()).singleton(); + } +} ``` ## Logging diff --git a/doc/quick_start_ru.md b/doc/quick_start_ru.md index 402fa6a..da51e8e 100644 --- a/doc/quick_start_ru.md +++ b/doc/quick_start_ru.md @@ -75,8 +75,54 @@ Scope - это контейнер, который хранит все дерев // или final str = rootScope.tryResolve(); - // закрыть главный 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 dispose() async { + // закрытие ресурса, соединений, таймеров и т.п., async/await + print('MyService disposed!'); + } +} + +final scope = openRootScope(); +scope.installModules([ + ModuleImpl(), +]); + +final service = scope.resolve(); + +// ... используем сервис ... + +// Рекомендуемый финал: +await Cherrypick.closeRootScope(); // выведет в консоль 'MyService disposed!' + +// Или для подскоупа: +await scope.closeSubScope('feature'); + +class ModuleImpl extends Module { + @override + void builder(Scope scope) { + bind().toProvide(() => MyService()).singleton(); + } +} ``` ## Логирование diff --git a/examples/client_app/pubspec.lock b/examples/client_app/pubspec.lock index 109876c..9650bbf 100644 --- a/examples/client_app/pubspec.lock +++ b/examples/client_app/pubspec.lock @@ -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: diff --git a/examples/postly/pubspec.lock b/examples/postly/pubspec.lock index 4101bf9..21d5810 100644 --- a/examples/postly/pubspec.lock +++ b/examples/postly/pubspec.lock @@ -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: From 547a15fa4ea79878cca61d977a91653488b82eab Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 4 Aug 2025 10:39:36 +0300 Subject: [PATCH 2/4] docs(faq): add best practice FAQ about using await with scope disposal - Added FAQ section in documentation (README and tutorials, EN + RU) recommending always using await when calling CherryPick.closeRootScope, scope.closeScope, or scope.dispose, even if no services implement Disposable. - Clarifies future-proof resource management for all users. --- cherrypick/README.md | 8 ++++++++ doc/full_tutorial_en.md | 10 ++++++++++ doc/full_tutorial_ru.md | 9 +++++++++ 3 files changed, 27 insertions(+) diff --git a/cherrypick/README.md b/cherrypick/README.md index 24664e9..1bb4e54 100644 --- a/cherrypick/README.md +++ b/cherrypick/README.md @@ -387,6 +387,14 @@ try { **More details:** See [cycle_detection.en.md](doc/cycle_detection.en.md) + +## FAQ + +### Q: Do I need to use `await` with CherryPick.closeRootScope(), scope.closeScope(), or scope.dispose() if I have no Disposable services? + +**A:** +Yes! Even if none of your services currently implement `Disposable`, always use `await` when closing scopes. If you later add resource cleanup (by implementing `dispose()`), CherryPick will handle it automatically without you needing to change your scope cleanup code. This ensures resource management is future-proof, robust, and covers all application scenarios. + ## Documentation - [Circular Dependency Detection (English)](doc/cycle_detection.en.md) diff --git a/doc/full_tutorial_en.md b/doc/full_tutorial_en.md index ac2fca4..f30e7e0 100644 --- a/doc/full_tutorial_en.md +++ b/doc/full_tutorial_en.md @@ -460,6 +460,16 @@ You can use CherryPick in Dart CLI, server apps, and microservices. All major fe | `@inject` | Auto-injection | Class fields | | `@scope` | Scope/realm | Class fields | + +--- + +## FAQ + +### Q: Do I need to use `await` with CherryPick.closeRootScope(), scope.closeScope(), or scope.dispose() if I have no Disposable services? + +**A:** +Yes! Even if none of your services currently implement `Disposable`, always use `await` when closing scopes. If you later add resource cleanup (by implementing `dispose()`), CherryPick will handle it automatically without you needing to change your scope cleanup code. This ensures resource management is future-proof, robust, and covers all application scenarios. + --- ## Useful Links diff --git a/doc/full_tutorial_ru.md b/doc/full_tutorial_ru.md index 13c20f6..21df6c8 100644 --- a/doc/full_tutorial_ru.md +++ b/doc/full_tutorial_ru.md @@ -465,6 +465,15 @@ void main() { --- +## FAQ + +### В: Нужно ли использовать `await` для CherryPick.closeRootScope(), scope.closeScope() или scope.dispose(), если ни один сервис не реализует Disposable? + +**О:** +Да! Даже если в данный момент ни один сервис не реализует Disposable, всегда используйте `await` при закрытии скоупа. Если в будущем потребуется добавить освобождение ресурсов через dispose, CherryPick вызовет его автоматически без изменения завершения работы ваших скоупов. Такой подход делает управление ресурсами устойчивым и безопасным для любых изменений архитектуры. + +--- + ## Полезные ссылки - [cherrypick](https://pub.dev/packages/cherrypick) From a4b0ddfa5413a6889ed1b0314e4668c05c9ec2c1 Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 4 Aug 2025 10:47:33 +0300 Subject: [PATCH 3/4] docs(faq): add best practice FAQ about using await with scope disposal - Added FAQ section in documentation (README and tutorials, EN + RU) recommending always using await when calling CherryPick.closeRootScope, scope.closeScope, or scope.dispose, even if no services implement Disposable. - Clarifies future-proof resource management for all users. --- cherrypick/README.md | 2 +- doc/full_tutorial_en.md | 2 +- doc/full_tutorial_ru.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cherrypick/README.md b/cherrypick/README.md index 1bb4e54..f3f75bc 100644 --- a/cherrypick/README.md +++ b/cherrypick/README.md @@ -390,7 +390,7 @@ try { ## FAQ -### Q: Do I need to use `await` with CherryPick.closeRootScope(), scope.closeScope(), or scope.dispose() if I have no Disposable services? +### Q: Do I need to use `await` with CherryPick.closeRootScope(), CherryPick.closeScope(), or scope.dispose() if I have no Disposable services? **A:** Yes! Even if none of your services currently implement `Disposable`, always use `await` when closing scopes. If you later add resource cleanup (by implementing `dispose()`), CherryPick will handle it automatically without you needing to change your scope cleanup code. This ensures resource management is future-proof, robust, and covers all application scenarios. diff --git a/doc/full_tutorial_en.md b/doc/full_tutorial_en.md index f30e7e0..f2e77ec 100644 --- a/doc/full_tutorial_en.md +++ b/doc/full_tutorial_en.md @@ -465,7 +465,7 @@ You can use CherryPick in Dart CLI, server apps, and microservices. All major fe ## FAQ -### Q: Do I need to use `await` with CherryPick.closeRootScope(), scope.closeScope(), or scope.dispose() if I have no Disposable services? +### Q: Do I need to use `await` with CherryPick.closeRootScope(), CherryPick.closeScope(), or scope.dispose() if I have no Disposable services? **A:** Yes! Even if none of your services currently implement `Disposable`, always use `await` when closing scopes. If you later add resource cleanup (by implementing `dispose()`), CherryPick will handle it automatically without you needing to change your scope cleanup code. This ensures resource management is future-proof, robust, and covers all application scenarios. diff --git a/doc/full_tutorial_ru.md b/doc/full_tutorial_ru.md index 21df6c8..46a0c2d 100644 --- a/doc/full_tutorial_ru.md +++ b/doc/full_tutorial_ru.md @@ -467,7 +467,7 @@ void main() { ## FAQ -### В: Нужно ли использовать `await` для CherryPick.closeRootScope(), scope.closeScope() или scope.dispose(), если ни один сервис не реализует Disposable? +### В: Нужно ли использовать `await` для CherryPick.closeRootScope(), CherryPick.closeScope() или scope.dispose(), если ни один сервис не реализует Disposable? **О:** Да! Даже если в данный момент ни один сервис не реализует Disposable, всегда используйте `await` при закрытии скоупа. Если в будущем потребуется добавить освобождение ресурсов через dispose, CherryPick вызовет его автоматически без изменения завершения работы ваших скоупов. Такой подход делает управление ресурсами устойчивым и безопасным для любых изменений архитектуры. From e5848784ac8e4857d5a515ff1c81b2a021a5d143 Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Fri, 8 Aug 2025 16:56:37 +0300 Subject: [PATCH 4/4] refactor(core): make closeRootScope async and await dispose BREAKING CHANGE: closeRootScope is now async (returns Future and must be awaited). This change improves resource lifecycle management by allowing asynchronous cleanup when disposing the root Scope. All usages of closeRootScope should be updated to use await. - Change closeRootScope from void to Future and add async keyword - Await _rootScopefor correct async disposal - Ensures proper disposal and better future extensibility for async resources Closes #xxx (replace if issue exists) --- cherrypick/lib/src/helper.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cherrypick/lib/src/helper.dart b/cherrypick/lib/src/helper.dart index 3795904..0828b73 100644 --- a/cherrypick/lib/src/helper.dart +++ b/cherrypick/lib/src/helper.dart @@ -94,9 +94,9 @@ class CherryPick { /// ```dart /// CherryPick.closeRootScope(); /// ``` - static void closeRootScope() { + static Future closeRootScope() async { if (_rootScope != null) { - _rootScope!.dispose(); // Автоматический вызов dispose для rootScope! + await _rootScope!.dispose(); // Автоматический вызов dispose для rootScope! _rootScope = null; } } @@ -252,9 +252,9 @@ class CherryPick { /// CherryPick.closeScope(scopeName: 'network.super.api'); /// ``` @experimental - static void closeScope({String scopeName = '', String separator = '.'}) { + static Future closeScope({String scopeName = '', String separator = '.'}) async { if (scopeName.isEmpty) { - closeRootScope(); + await closeRootScope(); return; } final nameParts = scopeName.split(separator); @@ -267,9 +267,9 @@ class CherryPick { openRootScope(), (Scope previous, String element) => previous.openSubScope(element) ); - scope.closeSubScope(lastPart); + await scope.closeSubScope(lastPart); } else { - openRootScope().closeSubScope(nameParts.first); + await openRootScope().closeSubScope(nameParts.first); } }