mirror of
https://github.com/pese-git/cherrypick.git
synced 2026-01-24 13:47:24 +00:00
Compare commits
2 Commits
async-opti
...
cherrypick
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cbcce5b38 | ||
|
|
264c4bbb88 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -3,6 +3,34 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## 2025-08-22
|
||||
|
||||
### Changes
|
||||
|
||||
---
|
||||
|
||||
Packages with breaking changes:
|
||||
|
||||
- There are no breaking changes in this release.
|
||||
|
||||
Packages with other changes:
|
||||
|
||||
- [`cherrypick_annotations` - `v1.1.2-dev.2`](#cherrypick_annotations---v112-dev2)
|
||||
- [`cherrypick_generator` - `v2.0.0-dev.2`](#cherrypick_generator---v200-dev2)
|
||||
|
||||
Packages with dependency updates only:
|
||||
|
||||
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
|
||||
|
||||
- `cherrypick_generator` - `v2.0.0-dev.2`
|
||||
|
||||
---
|
||||
|
||||
#### `cherrypick_annotations` - `v1.1.2-dev.2`
|
||||
|
||||
- **DOCS**(annotations): improve API documentation and usage example.
|
||||
|
||||
|
||||
## 2025-08-19
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -10,10 +10,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cherrypick/src/cycle_detector.dart';
|
||||
import 'package:cherrypick/src/disposable.dart';
|
||||
@@ -90,28 +88,6 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
// индекс для мгновенного поиска binding’ов
|
||||
final Map<Object, Map<String?, BindingResolver>> _bindingResolvers = {};
|
||||
|
||||
/// Cached [Future]s for async singleton or in-progress resolutions (keyed by binding).
|
||||
final Map<String, Future<Object?>> _asyncResolveCache = {};
|
||||
|
||||
/// Holds [Completer] for every async key currently being awaited — needed to notify all callers promptly and consistently in case of errors.
|
||||
final Map<String, Completer<Object?>> _asyncCompleterCache = {};
|
||||
|
||||
/// Tracks which async keys are actively in progress (to detect/guard against async circular dependencies).
|
||||
final Set<String> _activeAsyncKeys = {};
|
||||
|
||||
/// Converts parameter object to a unique key string for cache/indexing.
|
||||
String _paramsToKey(dynamic params) {
|
||||
if (params == null) return '';
|
||||
if (params is String) return params;
|
||||
if (params is num || params is bool) return params.toString();
|
||||
if (params is Map || params is List) return jsonEncode(params);
|
||||
return params.hashCode.toString();
|
||||
}
|
||||
|
||||
/// Builds a unique key from type, name, and parameters — used for async singleton/factory cache lookups.
|
||||
String _cacheKey<T>(String? named, [dynamic params]) =>
|
||||
'${T.toString()}:${named ?? ""}:${_paramsToKey(params)}';
|
||||
|
||||
/// Generates a unique identifier string for this scope instance.
|
||||
///
|
||||
/// Used internally for diagnostics, logging and global scope tracking.
|
||||
@@ -392,9 +368,6 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
result = await _resolveAsyncWithLocalDetection<T>(
|
||||
named: named, params: params);
|
||||
}
|
||||
//if (result == null) {
|
||||
// throw StateError('Can\'t resolve async dependency `$T`. Maybe you forget register it?');
|
||||
//}
|
||||
_trackDisposable(result);
|
||||
return result;
|
||||
}
|
||||
@@ -470,24 +443,10 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
/// Direct async resolution for [T] without cycle check. Returns null if missing. Internal use only.
|
||||
Future<T?> _tryResolveAsyncInternal<T>(
|
||||
{String? named, dynamic params}) async {
|
||||
final key = _cacheKey<T>(named, params);
|
||||
final resolver = _findBindingResolver<T>(named);
|
||||
if (resolver != null) {
|
||||
final isSingleton = resolver.isSingleton;
|
||||
return await _sharedAsyncResolveCurrentScope(
|
||||
key: key,
|
||||
resolver: resolver,
|
||||
isSingleton: isSingleton,
|
||||
params: params,
|
||||
);
|
||||
} else if (_parentScope != null) {
|
||||
// переход на родителя: выпадение из локального кэша!
|
||||
return await _parentScope.tryResolveAsync<T>(
|
||||
named: named, params: params);
|
||||
} else {
|
||||
// не найден — null, не кэшируем!
|
||||
return null;
|
||||
}
|
||||
// 1 - Try from own modules; 2 - Fallback to parent
|
||||
return resolver?.resolveAsync(params) ??
|
||||
_parentScope?.tryResolveAsync(named: named, params: params);
|
||||
}
|
||||
|
||||
/// Looks up the [BindingResolver] for [T] and [named] within this scope.
|
||||
@@ -516,117 +475,6 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared core for async binding resolution:
|
||||
/// Handles async singleton/factory caching, error propagation for all awaiting callers,
|
||||
/// and detection of async circular dependencies.
|
||||
///
|
||||
/// If an error occurs (circular or factory throws), all awaiting completions get the same error.
|
||||
/// For singletons, result stays in cache for next calls.
|
||||
///
|
||||
/// [key] — unique cache key for binding resolution (type:name:params)
|
||||
/// [resolver] — BindingResolver to provide async instance
|
||||
/// [isSingleton] — if true, caches the Future/result; otherwise cache is cleared after resolve
|
||||
/// [params] — (optional) parameters for resolution
|
||||
Future<T?> _sharedAsyncResolveCurrentScope<T>({
|
||||
required String key,
|
||||
required BindingResolver<T> resolver,
|
||||
required bool isSingleton,
|
||||
dynamic params,
|
||||
}) async {
|
||||
observer.onDiagnostic(
|
||||
'Async resolve requested',
|
||||
details: {
|
||||
'type': T.toString(),
|
||||
'key': key,
|
||||
'singleton': isSingleton,
|
||||
'params': params,
|
||||
'scopeId': scopeId,
|
||||
},
|
||||
);
|
||||
|
||||
if (_activeAsyncKeys.contains(key)) {
|
||||
observer.onDiagnostic(
|
||||
'Circular async DI detected',
|
||||
details: {
|
||||
'key': key,
|
||||
'asyncKeyStack': List<String>.from(_activeAsyncKeys)..add(key),
|
||||
'scopeId': scopeId
|
||||
},
|
||||
);
|
||||
final error = CircularDependencyException(
|
||||
'Circular async DI detected for key=$key',
|
||||
List<String>.from(_activeAsyncKeys)..add(key));
|
||||
if (_asyncCompleterCache.containsKey(key)) {
|
||||
final pending = _asyncCompleterCache[key]!;
|
||||
if (!pending.isCompleted) {
|
||||
pending.completeError(error, StackTrace.current);
|
||||
}
|
||||
} else {
|
||||
final completer = Completer<Object?>();
|
||||
_asyncCompleterCache[key] = completer;
|
||||
_asyncResolveCache[key] = completer.future;
|
||||
completer.completeError(error, StackTrace.current);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (_asyncResolveCache.containsKey(key)) {
|
||||
observer.onDiagnostic(
|
||||
'Async resolve cache HIT',
|
||||
details: {'key': key, 'scopeId': scopeId},
|
||||
);
|
||||
try {
|
||||
return (await _asyncResolveCache[key]) as T?;
|
||||
} catch (e) {
|
||||
observer.onDiagnostic(
|
||||
'Async resolve cache HIT — exception',
|
||||
details: {'key': key, 'scopeId': scopeId, 'error': e.toString()},
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
} else {
|
||||
observer.onDiagnostic(
|
||||
'Async resolve cache MISS',
|
||||
details: {'key': key, 'scopeId': scopeId},
|
||||
);
|
||||
}
|
||||
|
||||
final completer = Completer<Object?>();
|
||||
_asyncResolveCache[key] = completer.future;
|
||||
_asyncCompleterCache[key] = completer;
|
||||
_activeAsyncKeys.add(key);
|
||||
|
||||
try {
|
||||
observer.onDiagnostic('Async resolution started',
|
||||
details: {'key': key, 'scopeId': scopeId});
|
||||
final Future<T?> resultFut = resolver.resolveAsync(params)!;
|
||||
final T? result = await resultFut;
|
||||
observer.onDiagnostic('Async resolution success',
|
||||
details: {'key': key, 'scopeId': scopeId});
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(result);
|
||||
}
|
||||
if (!isSingleton) {
|
||||
_asyncResolveCache.remove(key);
|
||||
_asyncCompleterCache.remove(key);
|
||||
}
|
||||
return result;
|
||||
} catch (e, st) {
|
||||
observer.onDiagnostic('Async resolution error',
|
||||
details: {'key': key, 'scopeId': scopeId, 'error': e.toString()});
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(e, st);
|
||||
}
|
||||
_asyncResolveCache.remove(key);
|
||||
_asyncCompleterCache.remove(key);
|
||||
rethrow;
|
||||
} finally {
|
||||
observer.onDiagnostic('Async resolve FINISH (removing from active)',
|
||||
details: {'key': key, 'scopeId': scopeId});
|
||||
_activeAsyncKeys.remove(key); // всегда убираем!
|
||||
}
|
||||
}
|
||||
|
||||
/// Asynchronously disposes this [Scope], all tracked [Disposable] objects, and recursively
|
||||
/// all its child subscopes.
|
||||
///
|
||||
@@ -650,8 +498,5 @@ class Scope with CycleDetectionMixin, GlobalCycleDetectionMixin {
|
||||
await d.dispose();
|
||||
}
|
||||
_disposables.clear();
|
||||
_asyncResolveCache.clear();
|
||||
_asyncCompleterCache.clear();
|
||||
_activeAsyncKeys.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,24 +129,19 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'should detect cycles in async resolution',
|
||||
() async {
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.enableCycleDetection();
|
||||
test('should detect cycles in async resolution', () async {
|
||||
final scope = CherryPick.openRootScope();
|
||||
scope.enableCycleDetection();
|
||||
|
||||
scope.installModules([
|
||||
AsyncCircularModule(),
|
||||
]);
|
||||
scope.installModules([
|
||||
AsyncCircularModule(),
|
||||
]);
|
||||
|
||||
await expectLater(
|
||||
() => scope.resolveAsync<AsyncServiceA>(),
|
||||
throwsA(isA<CircularDependencyException>()),
|
||||
);
|
||||
},
|
||||
skip:
|
||||
'False positive [E] due to async cycle detection + Dart test runner bug',
|
||||
);
|
||||
expect(
|
||||
() => scope.resolveAsync<AsyncServiceA>(),
|
||||
throwsA(isA<CircularDependencyException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -267,19 +267,12 @@ void main() {
|
||||
final scope = Scope(null, observer: observer)
|
||||
..installModules([
|
||||
_InlineModule((m, s) {
|
||||
m.bind<int>().toProvideWithParams((x) async {
|
||||
print('[DEBUG] PARAMS: $x');
|
||||
return (x as int) * 3;
|
||||
});
|
||||
m.bind<int>().toProvideWithParams((x) async => (x as int) * 3);
|
||||
}),
|
||||
]);
|
||||
expect(await scope.resolveAsync<int>(params: 2), 6);
|
||||
final future = scope.resolveAsync<int>();
|
||||
await expectLater(
|
||||
() => future,
|
||||
throwsA(isA<StateError>()),
|
||||
);
|
||||
}, skip: true);
|
||||
expect(() => scope.resolveAsync<int>(), throwsA(isA<StateError>()));
|
||||
});
|
||||
test('tryResolveAsync returns null for missing', () async {
|
||||
final observer = MockObserver();
|
||||
final scope = Scope(null, observer: observer);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
## 1.1.2-dev.2
|
||||
|
||||
- **DOCS**(annotations): improve API documentation and usage example.
|
||||
|
||||
## 1.1.2-dev.1
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
111
cherrypick_annotations/example/example.dart
Normal file
111
cherrypick_annotations/example/example.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
// ignore: dangling_library_doc_comments
|
||||
/// Example using cherrypick_annotations together with cherrypick (core) and cherrypick_generator.
|
||||
|
||||
///
|
||||
/// Steps to use this example:
|
||||
/// 1. Make sure your example/pubspec.yaml contains:
|
||||
/// - cherrypick_annotations (this package)
|
||||
/// - cherrypick (core DI engine)
|
||||
/// - cherrypick_generator (as a dev_dependency)
|
||||
/// - build_runner (as a dev_dependency)
|
||||
/// 2. Run code generation to produce DI injectors and mixins:
|
||||
/// ```sh
|
||||
/// dart run build_runner build
|
||||
/// ```
|
||||
/// 3. The `_$ApiScreen` mixin will be generated automatically.
|
||||
/// 4. In your app/bootstrap code, install modules and use the generated features.
|
||||
///
|
||||
/// See documentation and advanced details at:
|
||||
/// https://pub.dev/packages/cherrypick_annotations
|
||||
|
||||
import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
// In a real project, use this import:
|
||||
// import 'package:cherrypick/cherrypick.dart';
|
||||
|
||||
// Temporary stub for demonstration purposes only.
|
||||
// In real usage, import 'Module' from `package:cherrypick/cherrypick.dart`.
|
||||
class Module {}
|
||||
|
||||
/// This mixin is a stub for documentation and IDE hints only.
|
||||
/// In a real project, it will be generated by cherrypick_generator after running build_runner.
|
||||
///
|
||||
/// Do not implement or edit this by hand!
|
||||
mixin _$ApiScreen {}
|
||||
|
||||
/// Example UI/service class with dependencies to be injected.
|
||||
///
|
||||
/// The [@injectable] annotation tells the generator to create an injector mixin for this class.
|
||||
/// Fields marked with [@inject] will be automatically filled by the code generator (using DI).
|
||||
@injectable()
|
||||
class ApiScreen with _$ApiScreen {
|
||||
/// The default (main) implementation of the API service.
|
||||
@inject()
|
||||
late final ApiService apiService;
|
||||
|
||||
/// An alternate API (mock) implementation, injected by name using @named.
|
||||
@inject()
|
||||
@named('mock')
|
||||
late final ApiService mockApiService;
|
||||
|
||||
/// Logger injected from another scope (e.g., global singleton).
|
||||
@inject()
|
||||
@scope('global')
|
||||
late final Logger logger;
|
||||
}
|
||||
|
||||
/// Example DI module using CherryPick annotations.
|
||||
///
|
||||
/// The [@module] annotation tells the generator to treat this class as a source of bindings.
|
||||
/// Methods annotated with [@singleton], [@named], [@provide], [@instance] will be registered into the DI container.
|
||||
@module()
|
||||
abstract class AppModule extends Module {
|
||||
/// Global singleton logger available throughout the app.
|
||||
@singleton()
|
||||
Logger provideLogger() => Logger();
|
||||
|
||||
/// Main API implementation, identified with the name 'main'.
|
||||
@named('main')
|
||||
ApiService createApi() => ApiService();
|
||||
|
||||
/// Mock API implementation, identified as 'mock'.
|
||||
@named('mock')
|
||||
ApiService createMockApi() => MockApiService();
|
||||
|
||||
/// UserManager is created with runtime parameters, such as per-user session.
|
||||
@provide()
|
||||
UserManager createManager(@params() Map<String, dynamic> runtimeParams) {
|
||||
return UserManager(runtimeParams['id'] as String);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Example implementations for demonstration only.
|
||||
// In a real project, these would contain application/service logic.
|
||||
|
||||
/// The main API service.
|
||||
class ApiService {}
|
||||
|
||||
/// A mock API implementation (for development or testing).
|
||||
class MockApiService extends ApiService {}
|
||||
|
||||
/// Manages user operations, created using dynamic (runtime) parameters.
|
||||
class UserManager {
|
||||
final String id;
|
||||
UserManager(this.id);
|
||||
}
|
||||
|
||||
/// Global logger service.
|
||||
class Logger {}
|
||||
|
||||
void main() {
|
||||
// After running code generation, injectors and mixins will be ready to use.
|
||||
// Example integration (pseudo-code):
|
||||
//
|
||||
// import 'package:cherrypick/cherrypick.dart';
|
||||
//
|
||||
// final scope = CherryPick.openRootScope()..installModules([$AppModule()]);
|
||||
// final screen = ApiScreen()..injectFields();
|
||||
// print(screen.apiService); // <-- injected!
|
||||
//
|
||||
// This main() is provided for reference only.
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
library;
|
||||
|
||||
//
|
||||
// Copyright 2021 Sergey Penkovsky (sergey.penkovsky@gmail.com)
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -13,6 +11,24 @@ library;
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
/// Annotations for use with the CherryPick dependency injection generator.
|
||||
///
|
||||
/// These annotations are used on classes, methods, fields or parameters to
|
||||
/// describe how they should participate in dependency injection.
|
||||
/// See: https://pub.dev/packages/cherrypick
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// import 'package:cherrypick_annotations/cherrypick_annotations.dart';
|
||||
///
|
||||
/// @injectable()
|
||||
/// class MyService {
|
||||
/// @inject()
|
||||
/// late final Logger logger;
|
||||
/// }
|
||||
/// ```
|
||||
library;
|
||||
|
||||
export 'src/module.dart';
|
||||
export 'src/provide.dart';
|
||||
export 'src/instance.dart';
|
||||
|
||||
@@ -38,5 +38,6 @@ import 'package:meta/meta.dart';
|
||||
/// ```
|
||||
@experimental
|
||||
final class inject {
|
||||
/// Creates an [inject] annotation for field injection.
|
||||
const inject();
|
||||
}
|
||||
|
||||
@@ -39,5 +39,6 @@ import 'package:meta/meta.dart';
|
||||
/// After running the generator, the mixin (`_\$ProfileScreen`) will be available to help auto-inject all [@inject] fields in your widget/service/controller.
|
||||
@experimental
|
||||
final class injectable {
|
||||
/// Creates an [injectable] annotation for classes.
|
||||
const injectable();
|
||||
}
|
||||
|
||||
@@ -45,5 +45,6 @@ import 'package:meta/meta.dart';
|
||||
/// See also: [@singleton]
|
||||
@experimental
|
||||
final class instance {
|
||||
/// Creates an [instance] annotation for classes or providers.
|
||||
const instance();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,6 @@ import 'package:meta/meta.dart';
|
||||
/// See also: [@singleton], [@instance], [@params], [@named]
|
||||
@experimental
|
||||
final class provide {
|
||||
/// Creates a [provide] annotation.
|
||||
/// Creates a [provide] annotation for marking provider methods/classes in DI modules.
|
||||
const provide();
|
||||
}
|
||||
|
||||
@@ -49,5 +49,7 @@ import 'package:meta/meta.dart';
|
||||
final class scope {
|
||||
/// The name/key of the DI scope from which to resolve this dependency.
|
||||
final String? name;
|
||||
|
||||
/// Creates a [scope] annotation specifying which DI scope to use for the dependency resolution.
|
||||
const scope(this.name);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: cherrypick_annotations
|
||||
description: |
|
||||
Set of annotations for CherryPick dependency injection library. Enables code generation and declarative DI for Dart & Flutter projects.
|
||||
version: 1.1.2-dev.1
|
||||
version: 1.1.2-dev.2
|
||||
homepage: https://cherrypick-di.dev/
|
||||
documentation: https://cherrypick-di.dev/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick/cherrypick_annotations
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
## 2.0.0-dev.2
|
||||
|
||||
- Update a dependency to the latest release.
|
||||
|
||||
## 2.0.0-dev.1
|
||||
|
||||
- **DOCS**(pub): update homepage and documentation URLs in pubspec.yaml to new official site.
|
||||
|
||||
@@ -2,7 +2,7 @@ name: cherrypick_generator
|
||||
description: |
|
||||
Source code generator for the cherrypick dependency injection system. Processes annotations to generate binding and module code for Dart & Flutter projects.
|
||||
|
||||
version: 2.0.0-dev.1
|
||||
version: 2.0.0-dev.2
|
||||
homepage: https://cherrypick-di.dev/
|
||||
documentation: https://cherrypick-di.dev/docs/intro
|
||||
repository: https://github.com/pese-git/cherrypick/cherrypick_generator
|
||||
@@ -19,7 +19,7 @@ environment:
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
cherrypick_annotations: ^1.1.2-dev.1
|
||||
cherrypick_annotations: ^1.1.2-dev.2
|
||||
analyzer: ^7.0.0
|
||||
dart_style: ^3.0.0
|
||||
build: ^2.4.1
|
||||
|
||||
14
pubspec.lock
14
pubspec.lock
@@ -5,23 +5,23 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "73.0.0"
|
||||
version: "76.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.2"
|
||||
version: "0.3.3"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.8.0"
|
||||
version: "6.11.0"
|
||||
ansi_styles:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -298,10 +298,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2-main.4"
|
||||
version: "0.1.3-main.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user