ark_di 0.1.0-dev.1
ark_di: ^0.1.0-dev.1 copied to clipboard
Pure Dart dependency injection with hierarchical scopes and explicit ownership.
example/ark_di_example.dart
import 'package:ark_di/ark_di.dart';
Future<void> main() async {
final List<String> lifecycle = <String>[];
final DiContainer application = DiContainer.build((binder) {
binder.bindInstance<AppConfig>(
const AppConfig(apiBaseUrl: 'https://api.example.com'),
);
binder.bindAsyncLazySingleton<ApiClient>(
(resolver) async {
final AppConfig config = resolver.get<AppConfig>();
return ApiClient.connect(config.apiBaseUrl);
},
dispose: (client) {
client.close();
lifecycle.add('api-client');
},
);
binder.bindAsyncLazySingleton<CatalogRepository>(
(resolver) async {
final ApiClient client = await resolver.getAsync<ApiClient>();
return RemoteCatalogRepository(client);
},
dispose: (repository) {
repository.close();
lifecycle.add('repository');
},
);
binder.bindAsyncLazySingleton<CatalogService>(
(resolver) async {
final CatalogRepository repository = await resolver
.getAsync<CatalogRepository>();
return CatalogService(repository);
},
dispose: (service) {
service.close();
lifecycle.add('service');
},
);
});
final CatalogService production = await application
.getAsync<CatalogService>();
final List<String> remoteBooks = await production.search('dart');
if (remoteBooks.single != 'Remote result for "dart"') {
throw StateError('The production graph returned an unexpected result.');
}
final DiContainer preview = application.createChild((binder) {
binder.bindInstance<CatalogRepository>(
MemoryCatalogRepository(<String>['Ark DI guide', 'UseCase Forge guide']),
overrideParent: true,
dispose: (repository) {
repository.close();
lifecycle.add('preview-repository');
},
);
binder.bindLazySingleton<CatalogService>(
(resolver) => CatalogService(resolver.get<CatalogRepository>()),
overrideParent: true,
dispose: (service) {
service.close();
lifecycle.add('preview-service');
},
);
});
final CatalogService previewService = preview.get<CatalogService>();
final List<String> previewBooks = await previewService.search('guide');
if (previewBooks.length != 2) {
throw StateError('The child scope did not use its local repository.');
}
await preview.close();
await application.close();
const List<String> expectedLifecycle = <String>[
'preview-service',
'preview-repository',
'service',
'repository',
'api-client',
];
if (!_sameItems(lifecycle, expectedLifecycle)) {
throw StateError(
'Unexpected teardown order: $lifecycle. Expected: $expectedLifecycle.',
);
}
print('Production: $remoteBooks');
print('Preview: $previewBooks');
print('Teardown: $lifecycle');
}
bool _sameItems(final List<String> left, final List<String> right) {
if (left.length != right.length) {
return false;
}
for (var index = 0; index < left.length; index += 1) {
if (left[index] != right[index]) {
return false;
}
}
return true;
}
final class AppConfig {
const AppConfig({required this.apiBaseUrl});
final String apiBaseUrl;
}
final class ApiClient {
ApiClient._(this.baseUrl);
final String baseUrl;
bool _closed = false;
static Future<ApiClient> connect(final String baseUrl) async {
await Future<void>.delayed(Duration.zero);
return ApiClient._(baseUrl);
}
Future<List<String>> search(final String query) async {
if (_closed) {
throw StateError('ApiClient is closed.');
}
return <String>['Remote result for "$query"'];
}
void close() {
_closed = true;
}
}
abstract interface class CatalogRepository {
Future<List<String>> search(String query);
void close();
}
final class RemoteCatalogRepository implements CatalogRepository {
RemoteCatalogRepository(this._client);
final ApiClient _client;
bool _closed = false;
@override
Future<List<String>> search(final String query) {
if (_closed) {
throw StateError('RemoteCatalogRepository is closed.');
}
return _client.search(query);
}
@override
void close() {
_closed = true;
}
}
final class MemoryCatalogRepository implements CatalogRepository {
MemoryCatalogRepository(this._items);
final List<String> _items;
bool _closed = false;
@override
Future<List<String>> search(final String query) async {
if (_closed) {
throw StateError('MemoryCatalogRepository is closed.');
}
final String normalized = query.toLowerCase();
return _items
.where((item) => item.toLowerCase().contains(normalized))
.toList(growable: false);
}
@override
void close() {
_closed = true;
}
}
final class CatalogService {
CatalogService(this._repository);
final CatalogRepository _repository;
bool _closed = false;
Future<List<String>> search(final String query) {
if (_closed) {
throw StateError('CatalogService is closed.');
}
return _repository.search(query);
}
void close() {
_closed = true;
}
}