Merge pull request #8 from pese-git/refactor

Refactor code and add toInstanceAsync method
This commit is contained in:
Sergey Penkovsky
2025-05-19 13:44:16 +03:00
committed by GitHub
7 changed files with 350 additions and 252 deletions

View File

@@ -30,7 +30,7 @@ Binding<String>().toProvide(() => "hello world");
// Asynchronous lazy initialization
Binding<String>().toProvideAsync(() async => "hello async world");
/ Asynchronous lazy initialization with dynamic parameters
// Asynchronous lazy initialization with dynamic parameters
Binding<String>().toProvideAsyncWithParams((params) async => "hello $params");
// Initialization with dynamic parameters

View File

@@ -13,6 +13,8 @@
enum Mode { simple, instance, providerInstance, providerInstanceWithParams }
typedef Provider<T> = T? Function();
typedef ProviderWithParams<T> = T Function(dynamic params);
typedef AsyncProvider<T> = Future<T> Function();
@@ -27,11 +29,13 @@ class Binding<T> {
late Type _key;
late String _name;
T? _instance;
T? Function()? _provider;
Future<T>? _instanceAsync;
Provider<T>? _provider;
ProviderWithParams<T>? _providerWithParams;
AsyncProvider<T>? asyncProvider;
AsyncProviderWithParams<T>? asyncProviderWithParams;
ProviderWithParams<T>? _providerWithParams;
late bool _isSingleton = false;
late bool _isNamed = false;
@@ -91,11 +95,22 @@ class Binding<T> {
return this;
}
/// RU: Инициализация экземляпяра [value].
/// ENG: Initialization instance [value].
///
/// return [Binding]
Binding<T> toInstanceAsync(Future<T> value) {
_mode = Mode.instance;
_instanceAsync = value;
_isSingleton = true;
return this;
}
/// RU: Инициализация экземляпяра  через провайдер [value].
/// ENG: Initialization instance via provider [value].
///
/// return [Binding]
Binding<T> toProvide(T Function() value) {
Binding<T> toProvide(Provider<T> value) {
_mode = Mode.providerInstance;
_provider = value;
return this;
@@ -146,6 +161,12 @@ class Binding<T> {
/// return [T]
T? get instance => _instance;
/// RU: Поиск экземпляра.
/// ENG: Resolve instance.
///
/// return [T]
Future<T>? get instanceAsync => _instanceAsync;
/// RU: Поиск экземпляра.
/// ENG: Resolve instance.
///

View File

@@ -160,11 +160,18 @@ class Scope {
if (binding.key == T &&
((!binding.isNamed && named == null) ||
(binding.isNamed && named == binding.name))) {
if (binding.instanceAsync != null) {
return await binding.instanceAsync;
}
if (binding.asyncProvider != null) {
return await binding.asyncProvider?.call();
}
if (binding.asyncProviderWithParams != null) {
if (params == null) {
throw StateError('Param is null. Maybe you forget pass it');
}
return await binding.asyncProviderWithParams!(params);
}
}

View File

@@ -2,314 +2,270 @@ import 'package:cherrypick/src/binding.dart';
import 'package:test/test.dart';
void main() {
group('Check instance.', () {
group('Without name.', () {
test('Binding resolves null', () {
// --- Instance binding (synchronous) ---
group('Instance Binding (toInstance)', () {
group('Without name', () {
test('Returns null by default', () {
final binding = Binding<int>();
expect(binding.instance, null);
});
test('Binding check mode', () {
final expectedValue = 5;
final binding = Binding<int>().toInstance(expectedValue);
test('Sets mode to instance', () {
final binding = Binding<int>().toInstance(5);
expect(binding.mode, Mode.instance);
});
test('Binding check singleton', () {
final expectedValue = 5;
final binding = Binding<int>().toInstance(expectedValue);
test('isSingleton is true', () {
final binding = Binding<int>().toInstance(5);
expect(binding.isSingleton, true);
});
test('Binding check value', () {
final expectedValue = 5;
final binding = Binding<int>().toInstance(expectedValue);
expect(binding.instance, expectedValue);
});
test('Binding resolves value', () {
final expectedValue = 5;
final binding = Binding<int>().toInstance(expectedValue);
expect(binding.instance, expectedValue);
test('Stores value', () {
final binding = Binding<int>().toInstance(5);
expect(binding.instance, 5);
});
});
group('With name.', () {
test('Binding resolves null', () {
final binding = Binding<int>().withName('expectedValue');
group('With name', () {
test('Returns null by default', () {
final binding = Binding<int>().withName('n');
expect(binding.instance, null);
});
test('Binding check mode', () {
final expectedValue = 5;
final binding =
Binding<int>().withName('expectedValue').toInstance(expectedValue);
test('Sets mode to instance', () {
final binding = Binding<int>().withName('n').toInstance(5);
expect(binding.mode, Mode.instance);
});
test('Binding check key', () {
final expectedValue = 5;
final binding =
Binding<int>().withName('expectedValue').toInstance(expectedValue);
test('Sets key', () {
final binding = Binding<int>().withName('n').toInstance(5);
expect(binding.key, int);
});
test('Binding check singleton', () {
final expectedValue = 5;
final binding =
Binding<int>().withName('expectedValue').toInstance(expectedValue);
test('isSingleton is true', () {
final binding = Binding<int>().withName('n').toInstance(5);
expect(binding.isSingleton, true);
});
test('Binding check value', () {
final expectedValue = 5;
final binding =
Binding<int>().withName('expectedValue').toInstance(expectedValue);
expect(binding.instance, expectedValue);
test('Stores value', () {
final binding = Binding<int>().withName('n').toInstance(5);
expect(binding.instance, 5);
});
test('Binding check value', () {
final expectedValue = 5;
final binding =
Binding<int>().withName('expectedValue').toInstance(expectedValue);
expect(binding.name, 'expectedValue');
test('Sets name', () {
final binding = Binding<int>().withName('n').toInstance(5);
expect(binding.name, 'n');
});
});
test('Binding resolves value', () {
final expectedValue = 5;
final binding =
Binding<int>().withName('expectedValue').toInstance(expectedValue);
expect(binding.instance, expectedValue);
});
test('Multiple toInstance calls change value', () {
final binding = Binding<int>().toInstance(1).toInstance(2);
expect(binding.instance, 2);
});
});
group('Check provide.', () {
group('Without name.', () {
test('Binding resolves null', () {
// --- Instance binding (asynchronous) ---
group('Async Instance Binding (toInstanceAsync)', () {
test('Resolves instanceAsync with expected value', () async {
final binding = Binding<int>().toInstanceAsync(Future.value(42));
expect(await binding.instanceAsync, 42);
});
test('Does not affect instance', () {
final binding = Binding<int>().toInstanceAsync(Future.value(5));
expect(binding.instance, null);
});
test('Sets mode to instance', () {
final binding = Binding<int>().toInstanceAsync(Future.value(5));
expect(binding.mode, Mode.instance);
});
test('isSingleton is true after toInstanceAsync', () {
final binding = Binding<int>().toInstanceAsync(Future.value(5));
expect(binding.isSingleton, isTrue);
});
test('Composes with withName', () async {
final binding = Binding<int>()
.withName('asyncValue')
.toInstanceAsync(Future.value(7));
expect(binding.isNamed, isTrue);
expect(binding.name, 'asyncValue');
expect(await binding.instanceAsync, 7);
});
test('Keeps value after multiple awaits', () async {
final binding = Binding<int>().toInstanceAsync(Future.value(123));
final result1 = await binding.instanceAsync;
final result2 = await binding.instanceAsync;
expect(result1, equals(result2));
});
});
// --- Provider binding (synchronous) ---
group('Provider Binding (toProvide)', () {
group('Without name', () {
test('Returns null by default', () {
final binding = Binding<int>();
expect(binding.provider, null);
});
test('Binding check mode', () {
final expectedValue = 5;
final binding = Binding<int>().toProvide(() => expectedValue);
test('Sets mode to providerInstance', () {
final binding = Binding<int>().toProvide(() => 5);
expect(binding.mode, Mode.providerInstance);
});
test('Binding check singleton', () {
final expectedValue = 5;
final binding = Binding<int>().toProvide(() => expectedValue);
test('isSingleton is false by default', () {
final binding = Binding<int>().toProvide(() => 5);
expect(binding.isSingleton, false);
});
test('Binding check value', () {
final expectedValue = 5;
final binding = Binding<int>().toProvide(() => expectedValue);
expect(binding.provider, expectedValue);
});
test('Binding resolves value', () {
final expectedValue = 5;
final binding = Binding<int>().toProvide(() => expectedValue);
expect(binding.provider, expectedValue);
test('Returns provided value', () {
final binding = Binding<int>().toProvide(() => 5);
expect(binding.provider, 5);
});
});
group('With name.', () {
test('Binding resolves null', () {
final binding = Binding<int>().withName('expectedValue');
group('With name', () {
test('Returns null by default', () {
final binding = Binding<int>().withName('n');
expect(binding.provider, null);
});
test('Binding check mode', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue);
test('Sets mode to providerInstance', () {
final binding = Binding<int>().withName('n').toProvide(() => 5);
expect(binding.mode, Mode.providerInstance);
});
test('Binding check key', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue);
test('Sets key', () {
final binding = Binding<int>().withName('n').toProvide(() => 5);
expect(binding.key, int);
});
test('Binding check singleton', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue);
test('isSingleton is false by default', () {
final binding = Binding<int>().withName('n').toProvide(() => 5);
expect(binding.isSingleton, false);
});
test('Binding check value', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue);
expect(binding.provider, expectedValue);
test('Returns provided value', () {
final binding = Binding<int>().withName('n').toProvide(() => 5);
expect(binding.provider, 5);
});
test('Binding check value', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue);
expect(binding.name, 'expectedValue');
});
test('Binding resolves value', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue);
expect(binding.provider, expectedValue);
test('Sets name', () {
final binding = Binding<int>().withName('n').toProvide(() => 5);
expect(binding.name, 'n');
});
});
});
group('Check Async provider.', () {
test('Binding resolves value asynchronously', () async {
final expectedValue = 5;
final binding = Binding<int>().toProvideAsync(() async => expectedValue);
final result = await binding.asyncProvider?.call();
expect(result, expectedValue);
// --- Async provider binding ---
group('Async Provider Binding', () {
test('Resolves asyncProvider value', () async {
final binding = Binding<int>().toProvideAsync(() async => 5);
expect(await binding.asyncProvider?.call(), 5);
});
test('Binding resolves value asynchronously with params', () async {
final expectedValue = 5;
final binding = Binding<int>().toProvideAsyncWithParams(
(param) async => expectedValue + (param as int));
final result = await binding.asyncProviderWithParams?.call(3);
expect(result, expectedValue + 3);
test('Resolves asyncProviderWithParams value', () async {
final binding = Binding<int>()
.toProvideAsyncWithParams((param) async => 5 + (param as int));
expect(await binding.asyncProviderWithParams?.call(3), 8);
});
});
group('Check singleton provide.', () {
group('Without name.', () {
test('Binding resolves null', () {
// --- Singleton provider binding ---
group('Singleton Provider Binding', () {
group('Without name', () {
test('Returns null if no provider set', () {
final binding = Binding<int>().singleton();
expect(binding.provider, null);
});
test('Binding check mode', () {
final expectedValue = 5;
final binding =
Binding<int>().toProvide(() => expectedValue).singleton();
test('Sets mode to providerInstance', () {
final binding = Binding<int>().toProvide(() => 5).singleton();
expect(binding.mode, Mode.providerInstance);
});
test('Binding check singleton', () {
final expectedValue = 5;
final binding =
Binding<int>().toProvide(() => expectedValue).singleton();
test('isSingleton is true', () {
final binding = Binding<int>().toProvide(() => 5).singleton();
expect(binding.isSingleton, true);
});
test('Binding check value', () {
final expectedValue = 5;
final binding =
Binding<int>().toProvide(() => expectedValue).singleton();
expect(binding.provider, expectedValue);
test('Returns singleton value', () {
final binding = Binding<int>().toProvide(() => 5).singleton();
expect(binding.provider, 5);
});
test('Binding resolves value', () {
final expectedValue = 5;
final binding =
Binding<int>().toProvide(() => expectedValue).singleton();
expect(binding.provider, expectedValue);
test('Returns same value each call and provider only called once', () {
int counter = 0;
final binding = Binding<int>().toProvide(() {
counter++;
return counter;
}).singleton();
final first = binding.provider;
final second = binding.provider;
expect(first, equals(second));
expect(counter, 1);
});
});
group('With name.', () {
test('Binding resolves null', () {
final binding = Binding<int>().withName('expectedValue').singleton();
group('With name', () {
test('Returns null if no provider set', () {
final binding = Binding<int>().withName('n').singleton();
expect(binding.provider, null);
});
test('Binding check mode', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue)
.singleton();
test('Sets mode to providerInstance', () {
final binding =
Binding<int>().withName('n').toProvide(() => 5).singleton();
expect(binding.mode, Mode.providerInstance);
});
test('Binding check key', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue)
.singleton();
test('Sets key', () {
final binding =
Binding<int>().withName('n').toProvide(() => 5).singleton();
expect(binding.key, int);
});
test('Binding check singleton', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue)
.singleton();
test('isSingleton is true', () {
final binding =
Binding<int>().withName('n').toProvide(() => 5).singleton();
expect(binding.isSingleton, true);
});
test('Binding check value', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue)
.singleton();
expect(binding.provider, expectedValue);
test('Returns singleton value', () {
final binding =
Binding<int>().withName('n').toProvide(() => 5).singleton();
expect(binding.provider, 5);
});
test('Binding check value', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue)
.singleton();
expect(binding.name, 'expectedValue');
test('Sets name', () {
final binding =
Binding<int>().withName('n').toProvide(() => 5).singleton();
expect(binding.name, 'n');
});
});
test('Binding resolves value', () {
final expectedValue = 5;
final binding = Binding<int>()
.withName('expectedValue')
.toProvide(() => expectedValue)
.singleton();
expect(binding.provider, expectedValue);
});
test('Chained withName and singleton preserves mode', () {
final binding =
Binding<int>().toProvide(() => 3).withName("named").singleton();
expect(binding.mode, Mode.providerInstance);
});
});
// --- WithName / Named binding, isNamed, edge-cases ---
group('Named binding & helpers', () {
test('withName sets isNamed true and stores name', () {
final binding = Binding<int>().withName('foo');
expect(binding.isNamed, true);
expect(binding.name, 'foo');
});
test('providerWithParams returns null if not set', () {
final binding = Binding<int>();
expect(binding.providerWithParams(123), null);
});
});
}

View File

@@ -3,46 +3,44 @@ import 'package:cherrypick/src/scope.dart';
import 'package:test/test.dart';
void main() {
group('Without parent scope.', () {
test('Parent scope is null.', () {
// --------------------------------------------------------------------------
group('Scope & Subscope Management', () {
test('Scope has no parent if constructed with null', () {
final scope = Scope(null);
expect(scope.parentScope, null);
});
test('Open sub scope.', () {
test('Can open and retrieve the same subScope by key', () {
final scope = Scope(null);
final subScope = scope.openSubScope('subScope');
expect(scope.openSubScope('subScope'), subScope);
});
test("Container throws state error if the value can't be resolved", () {
test('closeSubScope removes subscope so next openSubScope returns new', () {
final scope = Scope(null);
final subScope = scope.openSubScope("child");
expect(scope.openSubScope("child"), same(subScope));
scope.closeSubScope("child");
final newSubScope = scope.openSubScope("child");
expect(newSubScope, isNot(same(subScope)));
});
});
// --------------------------------------------------------------------------
group('Dependency Resolution (standard)', () {
test("Throws StateError if value can't be resolved", () {
final scope = Scope(null);
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
});
test('Container resolves value after adding a dependency', () {
test('Resolves value after adding a dependency', () {
final expectedValue = 'test string';
final scope = Scope(null)
.installModules([TestModule<String>(value: expectedValue)]);
expect(scope.resolve<String>(), expectedValue);
});
});
group('With parent scope.', () {
/*
test(
"Container bind() throws state error (if it's parent already has a resolver)",
() {
final parentScope = new Scope(null)
.installModules([TestModule<String>(value: "string one")]);
final scope = new Scope(parentScope);
expect(
() => scope.installModules([TestModule<String>(value: "string two")]),
throwsA(isA<StateError>()));
});
*/
test('Container resolve() returns a value from parent container.', () {
test('Returns a value from parent scope', () {
final expectedValue = 5;
final parentScope = Scope(null);
final scope = Scope(parentScope);
@@ -52,8 +50,7 @@ void main() {
expect(scope.resolve<int>(), expectedValue);
});
test('Container resolve() returns a several value from parent container.',
() {
test('Returns several values from parent container', () {
final expectedIntValue = 5;
final expectedStringValue = 'Hello world';
final parentScope = Scope(null).installModules([
@@ -66,15 +63,126 @@ void main() {
expect(scope.resolve<String>(), expectedStringValue);
});
test("Container resolve() throws a state error if parent hasn't value too.",
() {
test("Throws StateError if parent hasn't value too", () {
final parentScope = Scope(null);
final scope = Scope(parentScope);
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
});
test("After dropModules resolves fail", () {
final scope = Scope(null)..installModules([TestModule<int>(value: 5)]);
expect(scope.resolve<int>(), 5);
scope.dropModules();
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
});
});
// --------------------------------------------------------------------------
group('Named Dependencies', () {
test('Resolve named binding', () {
final scope = Scope(null)
..installModules([
TestModule<String>(value: "first"),
TestModule<String>(value: "second", name: "special")
]);
expect(scope.resolve<String>(named: "special"), "second");
expect(scope.resolve<String>(), "first");
});
test('Named binding does not clash with unnamed', () {
final scope = Scope(null)
..installModules([
TestModule<String>(value: "foo", name: "bar"),
]);
expect(() => scope.resolve<String>(), throwsA(isA<StateError>()));
expect(scope.resolve<String>(named: "bar"), "foo");
});
test("tryResolve returns null for missing named", () {
final scope = Scope(null)
..installModules([
TestModule<String>(value: "foo"),
]);
expect(scope.tryResolve<String>(named: "bar"), isNull);
});
});
// --------------------------------------------------------------------------
group('Provider with parameters', () {
test('Resolve dependency using providerWithParams', () {
final scope = Scope(null)
..installModules([
_InlineModule((m, s) {
m.bind<int>().toProvideWithParams((param) => (param as int) * 2);
}),
]);
expect(scope.resolve<int>(params: 3), 6);
expect(() => scope.resolve<int>(), throwsA(isA<StateError>()));
});
});
// --------------------------------------------------------------------------
group('Async Resolution', () {
test('Resolve async instance', () async {
final scope = Scope(null)
..installModules([
_InlineModule((m, s) {
m.bind<String>().toInstanceAsync(Future.value('async value'));
}),
]);
expect(await scope.resolveAsync<String>(), "async value");
});
test('Resolve async provider', () async {
final scope = Scope(null)
..installModules([
_InlineModule((m, s) {
m.bind<int>().toProvideAsync(() async => 7);
}),
]);
expect(await scope.resolveAsync<int>(), 7);
});
test('Resolve async provider with param', () async {
final scope = Scope(null)
..installModules([
_InlineModule((m, s) {
m.bind<int>().toProvideAsyncWithParams((x) async => (x as int) * 3);
}),
]);
expect(await scope.resolveAsync<int>(params: 2), 6);
expect(() => scope.resolveAsync<int>(), throwsA(isA<StateError>()));
});
test('tryResolveAsync returns null for missing', () async {
final scope = Scope(null);
final result = await scope.tryResolveAsync<String>();
expect(result, isNull);
});
});
// --------------------------------------------------------------------------
group('Optional resolution and error handling', () {
test("tryResolve returns null for missing dependency", () {
final scope = Scope(null);
expect(scope.tryResolve<int>(), isNull);
});
// Не реализован:
// test("Container bind() throws state error (if it's parent already has a resolver)", () {
// final parentScope = new Scope(null).installModules([TestModule<String>(value: "string one")]);
// final scope = new Scope(parentScope);
// expect(
// () => scope.installModules([TestModule<String>(value: "string two")]),
// throwsA(isA<StateError>()));
// });
});
}
// ----------------------------------------------------------------------------
// Вспомогательные модули
class TestModule<T> extends Module {
final T value;
final String? name;
@@ -89,3 +197,12 @@ class TestModule<T> extends Module {
}
}
}
/// Вспомогательный модуль для подстановки builder'а через конструктор
class _InlineModule extends Module {
final void Function(Module, Scope) _builder;
_InlineModule(this._builder);
@override
void builder(Scope s) => _builder(this, s);
}

View File

@@ -28,18 +28,16 @@ packages:
cherrypick:
dependency: "direct main"
description:
name: cherrypick
sha256: e1e2b4f3a70cbe7760e479e6ddb7dce2fc85a1bbb2fba6c398efe235ed111dfe
url: "https://pub.dev"
source: hosted
version: "2.0.2"
path: "../../cherrypick"
relative: true
source: path
version: "2.1.0"
cherrypick_flutter:
dependency: "direct main"
description:
name: cherrypick_flutter
sha256: ad63ae816b7d1147ffb0a82bcae5a1ea3d51e9d398a79459c619464391a43a79
url: "https://pub.dev"
source: hosted
path: "../../cherrypick_flutter"
relative: true
source: path
version: "1.1.1"
clock:
dependency: transitive

View File

@@ -153,11 +153,10 @@ packages:
cherrypick:
dependency: "direct main"
description:
name: cherrypick
sha256: e1e2b4f3a70cbe7760e479e6ddb7dce2fc85a1bbb2fba6c398efe235ed111dfe
url: "https://pub.dev"
source: hosted
version: "2.0.2"
path: "../../cherrypick"
relative: true
source: path
version: "2.1.0"
clock:
dependency: transitive
description: