diff --git a/cherrypick/.gitignore b/cherrypick/.gitignore index dbef116..16cc011 100644 --- a/cherrypick/.gitignore +++ b/cherrypick/.gitignore @@ -19,3 +19,6 @@ doc/api/ *.js_ *.js.deps *.js.map + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/cherrypick/README.md b/cherrypick/README.md index 8f76c97..a4499cb 100644 --- a/cherrypick/README.md +++ b/cherrypick/README.md @@ -1,31 +1,43 @@ # CherryPick Flutter -`cherrypick_flutter` is a powerful Flutter library for managing and accessing dependencies within your application through a root scope context using `CherryPickProvider`. It offers simplified dependency injection, making your application more modular and test-friendly. +`cherrypick_flutter` is a robust Flutter library designed for managing and accessing dependencies using a scope context provided by `CherryPickProvider`. It enhances your application's modularity and testability by simplifying dependency injection. ## Quick Start -### Main Components in Dependency Injection (DI) +### Core Components of Dependency Injection (DI) #### Binding -A Binding is a custom instance configurator, essential for setting up dependencies. It offers the following key methods: +A Binding is a custom instance configurator crucial for setting up dependencies. It offers the following key methods: - `toInstance()`: Directly provides an initialized instance. -- `toProvide()`: Accepts a provider function or constructor for lazy initialization. -- `withName()`: Assigns a name to an instance, allowing for retrieval by name. -- `singleton()`: Marks the instance as a singleton, ensuring only one instance exists in the scope. +- `toProvide()`: Accepts a provider function for lazy initialization. +- `toProvideAsync()`: Accepts an asynchronous provider for lazy initialization. +- `toProvideWithParams()`: Accepts a provider function requiring dynamic parameters. +- `toProvideAsyncWithParams()`: Accepts an asynchronous provider requiring dynamic parameters. +- `withName()`: Assigns a name for instance retrieval by name. +- `singleton()`: Marks the instance as a singleton, ensuring only one instance exists within the scope. ##### Example: ```dart -// Direct instance initialization with toInstance() +// Direct instance initialization using toInstance() Binding().toInstance("hello world"); -// Or use a provider for lazy initialization +// Lazy initialization via provider Binding().toProvide(() => "hello world"); -// Named instance -Binding().withName("my_string").toInstance("hello world"); +// Asynchronous lazy initialization +Binding().toProvideAsync(() async => "hello async world"); + +/ Asynchronous lazy initialization with dynamic parameters +Binding().toProvideAsyncWithParams((params) async => "hello $params"); + +// Initialization with dynamic parameters +Binding().toProvideWithParams((params) => "hello $params"); + +// Named instance for resolution +Binding().toProvide(() => "hello world").withName("my_string").toInstance("hello world"); // Singleton instance Binding().toProvide(() => "hello world").singleton(); @@ -33,7 +45,7 @@ Binding().toProvide(() => "hello world").singleton(); #### Module -A Module encapsulates bindings, allowing you to organize dependencies logically. To create a custom module, implement the `void builder(Scope currentScope)` method. +A Module encapsulates bindings, logically organizing dependencies. Implement the `void builder(Scope currentScope)` method to create a custom module. ##### Example: @@ -48,13 +60,13 @@ class AppModule extends Module { #### Scope -A Scope is the container that manages your dependency tree, holding modules and instances. Use the scope to access dependencies with the `resolve()` method. +A Scope manages your dependency tree, holding modules and instances. Use the scope to access dependencies with `resolve()` or `resolveAsync()` for asynchronous operations. ##### Example: ```dart // Open the main scope -final rootScope = Cherrypick.openRootScope(); +final rootScope = CherryPick.openRootScope(); // Install custom modules rootScope.installModules([AppModule()]); @@ -62,13 +74,16 @@ rootScope.installModules([AppModule()]); // Resolve an instance final str = rootScope.resolve(); +// Asynchronously resolve an instance +final asyncStr = await rootScope.resolveAsync(); + // Close the main scope -Cherrypick.closeRootScope(); +CherryPick.closeRootScope(); ``` ## Example Application -The following example demonstrates module setup, scope management, and dependency resolution. +The following example demonstrates module setup, scope management, and dependency resolution (both synchronous and asynchronous). ```dart import 'dart:async'; @@ -84,26 +99,30 @@ class AppModule extends Module { } class FeatureModule extends Module { - bool isMock; + final bool isMock; FeatureModule({required this.isMock}); @override void builder(Scope currentScope) { + // Using toProvideAsync for async initialization bind() .withName("networkRepo") - .toProvide( - () => NetworkDataRepository( - currentScope.resolve( - named: isMock ? "apiClientMock" : "apiClientImpl", - ), - ), - ) + .toProvideAsync(() async { + final client = await Future.delayed( + Duration(milliseconds: 100), + () => currentScope.resolve( + named: isMock ? "apiClientMock" : "apiClientImpl")); + return NetworkDataRepository(client); + }) .singleton(); - bind().toProvide( - () => DataBloc( - currentScope.resolve(named: "networkRepo"), - ), + + // Asynchronous initialization of DataBloc + bind().toProvideAsync( + () async { + final repo = await currentScope.resolveAsync(named: "networkRepo"); + return DataBloc(repo); + }, ); } } @@ -117,7 +136,8 @@ void main() async { .openSubScope("featureScope") .installModules([FeatureModule(isMock: true)]); - final dataBloc = subScope.resolve(); + // Asynchronous instance resolution + final dataBloc = await subScope.resolveAsync(); dataBloc.data.listen((d) => print('Received data: $d'), onError: (e) => print('Error: $e'), onDone: () => print('DONE')); @@ -128,7 +148,7 @@ class DataBloc { final DataRepository _dataRepository; Stream get data => _dataController.stream; - StreamController _dataController = new StreamController.broadcast(); + StreamController _dataController = StreamController.broadcast(); DataBloc(this._dataRepository); @@ -185,6 +205,8 @@ class ApiClientImpl implements ApiClient { - [x] Main Scope and Sub Scopes - [x] Named Instance Initialization +- [x] Asynchronous Dependency Resolution +- [x] Dynamic Parameter Support for Providers ## Contributing diff --git a/cherrypick/example/bin/main.dart b/cherrypick/example/bin/main.dart index c57e00f..d797948 100644 --- a/cherrypick/example/bin/main.dart +++ b/cherrypick/example/bin/main.dart @@ -1,39 +1,38 @@ import 'dart:async'; - -import 'package:cherrypick/cherrypick.dart'; import 'package:meta/meta.dart'; +import 'package:cherrypick/cherrypick.dart'; class AppModule extends Module { @override void builder(Scope currentScope) { - bind().withName('apiClientMock').toInstance(ApiClientMock()); - bind().withName('apiClientImpl').toInstance(ApiClientImpl()); + bind().withName("apiClientMock").toInstance(ApiClientMock()); + bind().withName("apiClientImpl").toInstance(ApiClientImpl()); } } class FeatureModule extends Module { - bool isMock; + final bool isMock; FeatureModule({required this.isMock}); @override void builder(Scope currentScope) { - bind() - .withName('networkRepo') - .toProvide( - () => NetworkDataRepository( - currentScope.resolve( - named: isMock ? 'apiClientMock' : 'apiClientImpl', - ), - ), - ) - .singleton(); + // Using toProvideAsync for async initialization + bind().withName("networkRepo").toProvideAsync(() async { + final client = await Future.delayed( + Duration(milliseconds: 100), + () => currentScope.resolve( + named: isMock ? "apiClientMock" : "apiClientImpl")); + return NetworkDataRepository(client); + }).singleton(); - bind().toProvideWithParams( - (param) => DataBloc( - currentScope.resolve(named: 'networkRepo'), - param, - ), + // Asynchronous initialization of DataBloc + bind().toProvideAsync( + () async { + final repo = await currentScope.resolveAsync( + named: "networkRepo"); + return DataBloc(repo); + }, ); } } @@ -44,10 +43,11 @@ void main() async { ]); final subScope = scope - .openSubScope('featureScope') + .openSubScope("featureScope") .installModules([FeatureModule(isMock: true)]); - final dataBloc = subScope.resolve(params: 'PARAMETER'); + // Asynchronous instance resolution + final dataBloc = await subScope.resolveAsync(); dataBloc.data.listen((d) => print('Received data: $d'), onError: (e) => print('Error: $e'), onDone: () => print('DONE')); @@ -60,13 +60,11 @@ class DataBloc { Stream get data => _dataController.stream; final StreamController _dataController = StreamController.broadcast(); - final String param; - - DataBloc(this._dataRepository, this.param); + DataBloc(this._dataRepository); Future fetchData() async { try { - _dataController.sink.add(await _dataRepository.getData(param)); + _dataController.sink.add(await _dataRepository.getData()); } catch (e) { _dataController.sink.addError(e); } @@ -78,7 +76,7 @@ class DataBloc { } abstract class DataRepository { - Future getData(String param); + Future getData(); } class NetworkDataRepository implements DataRepository { @@ -88,42 +86,26 @@ class NetworkDataRepository implements DataRepository { NetworkDataRepository(this._apiClient); @override - Future getData(String param) async => await _apiClient.sendRequest( - url: 'www.google.com', - token: _token, - requestBody: {'type': 'data'}, - param: param); + Future getData() async => await _apiClient.sendRequest( + url: 'www.google.com', token: _token, requestBody: {'type': 'data'}); } abstract class ApiClient { - Future sendRequest({ - @required String url, - String token, - Map requestBody, - String param, - }); + Future sendRequest({@required String url, String token, Map requestBody}); } class ApiClientMock implements ApiClient { @override - Future sendRequest({ - @required String? url, - String? token, - Map? requestBody, - String? param, - }) async { - return 'Local Data $param'; + Future sendRequest( + {@required String? url, String? token, Map? requestBody}) async { + return 'Local Data'; } } class ApiClientImpl implements ApiClient { @override - Future sendRequest({ - @required String? url, - String? token, - Map? requestBody, - String? param, - }) async { - return 'Network data $param'; + Future sendRequest( + {@required String? url, String? token, Map? requestBody}) async { + return 'Network data'; } } diff --git a/cherrypick/lib/cherrypick.dart b/cherrypick/lib/cherrypick.dart index 053877e..f229438 100644 --- a/cherrypick/lib/cherrypick.dart +++ b/cherrypick/lib/cherrypick.dart @@ -1,3 +1,5 @@ +library; + /// /// Copyright 2021 Sergey Penkovsky /// Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,8 +13,6 @@ /// limitations under the License. /// -library cherrypick; - export 'package:cherrypick/src/binding.dart'; export 'package:cherrypick/src/helper.dart'; export 'package:cherrypick/src/module.dart'; diff --git a/cherrypick/lib/src/binding.dart b/cherrypick/lib/src/binding.dart index 278d133..4ce8d0c 100644 --- a/cherrypick/lib/src/binding.dart +++ b/cherrypick/lib/src/binding.dart @@ -1,20 +1,24 @@ -/// -/// Copyright 2021 Sergey Penkovsky -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// http://www.apache.org/licenses/LICENSE-2.0 -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// +// +// Copyright 2021 Sergey Penkovsky +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// enum Mode { simple, instance, providerInstance, providerInstanceWithParams } typedef ProviderWithParams = T Function(dynamic params); +typedef AsyncProvider = Future Function(); + +typedef AsyncProviderWithParams = Future Function(dynamic params); + /// RU: Класс Binding настраивает параметры экземпляра. /// ENG: The Binding class configures the settings for the instance. /// @@ -24,6 +28,9 @@ class Binding { late String _name; T? _instance; T? Function()? _provider; + AsyncProvider? asyncProvider; + AsyncProviderWithParams? asyncProviderWithParams; + ProviderWithParams? _providerWithParams; late bool _isSingleton = false; late bool _isNamed = false; @@ -94,6 +101,16 @@ class Binding { return this; } + /// RU: Инициализация экземляпяра  через провайдер [value]. + /// ENG: Initialization instance via provider [value]. + /// + /// return [Binding] + Binding toProvideAsync(AsyncProvider provider) { + _mode = Mode.providerInstance; + asyncProvider = provider; + return this; + } + /// RU: Инициализация экземляпяра  через провайдер [value] c динамическим параметром. /// ENG: Initialization instance via provider [value] with a dynamic param. /// @@ -104,6 +121,16 @@ class Binding { return this; } + /// RU: Инициализация экземляра через асинхронный провайдер [value] с динамическим параметром. + /// ENG: Initializes the instance via async provider [value] with a dynamic param. + /// + /// return [Binding] + Binding toProvideAsyncWithParams(AsyncProviderWithParams provider) { + _mode = Mode.providerInstanceWithParams; + asyncProviderWithParams = provider; + return this; + } + /// RU: Инициализация экземляпяра  как сингелтон [value]. /// ENG: Initialization instance as a singelton [value]. /// diff --git a/cherrypick/lib/src/factory.dart b/cherrypick/lib/src/factory.dart index f2bbb89..cbf9d4c 100644 --- a/cherrypick/lib/src/factory.dart +++ b/cherrypick/lib/src/factory.dart @@ -1,15 +1,15 @@ -/// -/// Copyright 2021 Sergey Penkovsky -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// http://www.apache.org/licenses/LICENSE-2.0 -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// +// +// Copyright 2021 Sergey Penkovsky +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// import 'package:cherrypick/src/scope.dart'; abstract class Factory { diff --git a/cherrypick/lib/src/helper.dart b/cherrypick/lib/src/helper.dart index 3295c96..e9e2895 100644 --- a/cherrypick/lib/src/helper.dart +++ b/cherrypick/lib/src/helper.dart @@ -1,15 +1,15 @@ -/// -/// Copyright 2021 Sergey Penkovsky -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// http://www.apache.org/licenses/LICENSE-2.0 -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// +// +// Copyright 2021 Sergey Penkovsky +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// import 'package:cherrypick/src/scope.dart'; import 'package:meta/meta.dart'; diff --git a/cherrypick/lib/src/module.dart b/cherrypick/lib/src/module.dart index 32be9d9..0eab282 100644 --- a/cherrypick/lib/src/module.dart +++ b/cherrypick/lib/src/module.dart @@ -1,15 +1,15 @@ -/// -/// Copyright 2021 Sergey Penkovsky -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// http://www.apache.org/licenses/LICENSE-2.0 -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// +// +// Copyright 2021 Sergey Penkovsky +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// import 'dart:collection'; import 'package:cherrypick/src/binding.dart'; diff --git a/cherrypick/lib/src/scope.dart b/cherrypick/lib/src/scope.dart index 575f9e7..ca53dcb 100644 --- a/cherrypick/lib/src/scope.dart +++ b/cherrypick/lib/src/scope.dart @@ -1,15 +1,15 @@ -/// -/// Copyright 2021 Sergey Penkovsky -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// http://www.apache.org/licenses/LICENSE-2.0 -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// +// +// Copyright 2021 Sergey Penkovsky +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// import 'dart:collection'; import 'package:cherrypick/src/binding.dart'; @@ -132,4 +132,45 @@ class Scope { // 2 Поиск зависимостей в родительском скоупе return _parentScope?.tryResolve(named: named); } + + /// RU: Асинхронно возвращает найденную зависимость, определенную параметром типа [T]. + /// Выдает [StateError], если зависимость не может быть разрешена. + /// Если хотите получить [null], если зависимость не может быть найдена, используйте [tryResolveAsync]. + /// return - возвращает объект типа [T] or [StateError] + /// + /// ENG: Asynchronously returns the found dependency specified by the type parameter [T]. + /// Throws [StateError] if the dependency cannot be resolved. + /// If you want to get [null] if the dependency cannot be found, use [tryResolveAsync] instead. + /// return - returns an object of type [T] or [StateError] + /// + Future resolveAsync({String? named, dynamic params}) async { + var resolved = await tryResolveAsync(named: named, params: params); + if (resolved != null) { + return resolved; + } else { + throw StateError( + 'Can\'t resolve async dependency `$T`. Maybe you forget register it?'); + } + } + + Future tryResolveAsync({String? named, dynamic params}) async { + if (_modulesList.isNotEmpty) { + for (var module in _modulesList) { + for (var binding in module.bindingSet) { + if (binding.key == T && + ((!binding.isNamed && named == null) || + (binding.isNamed && named == binding.name))) { + if (binding.asyncProvider != null) { + return await binding.asyncProvider?.call(); + } + + if (binding.asyncProviderWithParams != null) { + return await binding.asyncProviderWithParams!(params); + } + } + } + } + } + return _parentScope?.tryResolveAsync(named: named, params: params); + } } diff --git a/cherrypick/pubspec.yaml b/cherrypick/pubspec.yaml index 516103c..ad17d0a 100644 --- a/cherrypick/pubspec.yaml +++ b/cherrypick/pubspec.yaml @@ -13,9 +13,8 @@ dependencies: meta: ^1.3.0 dev_dependencies: - #pedantic: ^1.11.0 - - test: ^1.17.2 + lints: ^5.0.0 + test: ^1.25.15 mockito: ^5.0.6 - lints: ^2.1.0 + melos: ^6.3.2 diff --git a/cherrypick/test/src/binding_test.dart b/cherrypick/test/src/binding_test.dart index 34d6c60..9f69647 100644 --- a/cherrypick/test/src/binding_test.dart +++ b/cherrypick/test/src/binding_test.dart @@ -188,6 +188,25 @@ void main() { }); }); + group('Check Async provider.', () { + test('Binding resolves value asynchronously', () async { + final expectedValue = 5; + final binding = Binding().toProvideAsync(() async => expectedValue); + + final result = await binding.asyncProvider?.call(); + expect(result, expectedValue); + }); + + test('Binding resolves value asynchronously with params', () async { + final expectedValue = 5; + final binding = Binding().toProvideAsyncWithParams( + (param) async => expectedValue + (param as int)); + + final result = await binding.asyncProviderWithParams?.call(3); + expect(result, expectedValue + 3); + }); + }); + group('Check singleton provide.', () { group('Without name.', () { test('Binding resolves null', () { diff --git a/cherrypick_flutter/lib/cherrypick_flutter.dart b/cherrypick_flutter/lib/cherrypick_flutter.dart index 8085ee2..48fdf3a 100644 --- a/cherrypick_flutter/lib/cherrypick_flutter.dart +++ b/cherrypick_flutter/lib/cherrypick_flutter.dart @@ -1,3 +1,5 @@ +library; + /// /// Copyright 2021 Sergey Penkovsky /// Licensed under the Apache License, Version 2.0 (the "License"); @@ -10,6 +12,5 @@ /// See the License for the specific language governing permissions and /// limitations under the License. /// -library cherrypick_flutter; export 'src/cherrypick_provider.dart'; diff --git a/cherrypick_flutter/pubspec.yaml b/cherrypick_flutter/pubspec.yaml index 9aa7748..176364d 100644 --- a/cherrypick_flutter/pubspec.yaml +++ b/cherrypick_flutter/pubspec.yaml @@ -18,7 +18,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 + flutter_lints: ^5.0.0 + test: ^1.25.7 + melos: ^6.3.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/melos.yaml b/melos.yaml index b0165cf..666468b 100644 --- a/melos.yaml +++ b/melos.yaml @@ -8,8 +8,10 @@ packages: scripts: analyze: - run: | - flutter analyze + exec: dart analyze + format: - run: | - flutter format \ No newline at end of file + exec: dart format + + test: + exec: flutter test \ No newline at end of file