From a9c95f6a898f1b72ff5c03e2ed8921f77aeba3fb Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Fri, 1 Aug 2025 15:07:12 +0300 Subject: [PATCH] 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: