dart_subsystem 0.1.2
dart_subsystem: ^0.1.2 copied to clipboard
A lightweight, lifecycle-safe framework to manage singleton subsystems and services in Dart applications.
import 'package:dart_subsystem/dart_subsystem.dart';
// 1. Define Category/Type descriptor
final networkTypeDesc = SubsystemTypeDesc(
displayName: 'Network Services',
description: 'Services managing communication and network I/O',
serviceTypeId: 'core.network',
);
// 2. Define concrete Subsystem implementation descriptor
final apiClientClassDesc = SubsystemClassDesc(
serviceType: networkTypeDesc,
displayName: 'API Client Subsystem',
description: 'Manages HTTP/REST calls and connection state',
serviceClassId: 'network.api_client',
defaultBuilder: (params) async => ApiClientSubsystem(classDesc: params.classDescription),
);
// 3. Subsystem implementation
class ApiClientSubsystem extends Subsystem {
ApiClientSubsystem({required super.classDesc});
bool _isConnected = false;
bool get isConnected => _isConnected;
@override
void beginConstruct(BuildServiceParameters params) {
// Synchronous setup right after instantiation
print('[ApiClient] Constructing subsystem...');
}
@override
Future<void> initialize(BuildServiceParameters params) async {
// Asynchronous loading or handshake
print('[ApiClient] Connecting to remote service...');
await Future<void>.delayed(const Duration(milliseconds: 50));
_isConnected = true;
print('[ApiClient] Connected successfully!');
}
void fetchData(String endpoint) {
// Protect against usage after disposal
assertNotDisposed();
print('[ApiClient] Fetching data from: $endpoint');
}
@override
Future<void> dispose() async {
// Release resources, disconnect connections
print('[ApiClient] Disconnecting...');
_isConnected = false;
}
@override
Future<void> finalDispose() async {
// Final cleanup guaranteed to execute even if dispose throws
print('[ApiClient] Final cleanup complete.');
}
}
// --- Shared-type, multi-implementation example -----------------------------
//
// A shared package only ever references [notificationsTypeDesc]. It has no
// idea which concrete class each consuming app registers behind it — that's
// entirely up to the app. This is what decouples a shared package from any
// single app's implementation.
final notificationsTypeDesc = SubsystemTypeDesc(
displayName: 'Notifications',
description: 'Delivers local or push notifications to the user',
serviceTypeId: 'core.notifications',
);
abstract class NotificationsSubsystem extends Subsystem {
NotificationsSubsystem({required super.classDesc});
void notify(String message);
}
// App "mobile" registers a local-notifications implementation...
class LocalNotificationsSubsystem extends NotificationsSubsystem {
LocalNotificationsSubsystem({required super.classDesc});
@override
void notify(String message) => print('[Local] $message');
}
// ...while app "web" would register a completely different one, behind the
// same type (not registered in this example, shown for illustration only).
class WebPushNotificationsSubsystem extends NotificationsSubsystem {
WebPushNotificationsSubsystem({required super.classDesc});
@override
void notify(String message) => print('[WebPush] $message');
}
final localNotificationsClassDesc = SubsystemClassDesc(
serviceType: notificationsTypeDesc,
displayName: 'Local Notifications',
description: 'OS-level local notifications',
serviceClassId: 'notifications.local',
defaultBuilder: (params) async => LocalNotificationsSubsystem(classDesc: params.classDescription),
);
// --- Capability interface + global lookup -----------------------------------
//
// `allInstancesOfClass`/`allInstancesWhere` scan every registered type, so a
// generic "boot participant" style interface can be implemented by subsystems
// of any type, without the caller enumerating types manually.
abstract class BootParticipant {
void onBootCompleted();
}
class LoggingSubsystem extends Subsystem implements BootParticipant {
LoggingSubsystem({required super.classDesc});
@override
void onBootCompleted() => print('[Logging] Boot completed, ready to log.');
}
final loggingTypeDesc = SubsystemTypeDesc(
displayName: 'Logging',
description: 'Application-wide logging sink',
serviceTypeId: 'core.logging',
);
final loggingClassDesc = SubsystemClassDesc(
serviceType: loggingTypeDesc,
displayName: 'Logging Subsystem',
description: 'Writes logs to stdout',
serviceClassId: 'logging.stdout',
defaultBuilder: (params) async => LoggingSubsystem(classDesc: params.classDescription),
);
Future<void> main() async {
// Optional: hook into lifecycle errors during disposal
SubsystemInstanceRegistry.onLifecycleError = (error, stackTrace) {
print('[Lifecycle Error] $error');
};
// Build and register subsystems using the factory
final factory = TSubsystemFactory();
factory.addServices([
BuildServiceParameters(classDescription: apiClientClassDesc),
BuildServiceParameters(classDescription: localNotificationsClassDesc),
BuildServiceParameters(classDescription: loggingClassDesc),
]);
print('Registering subsystems...');
await factory.registerSubsystems();
// Retrieve subsystem instance synchronously
final client = apiClientClassDesc.getInstance<ApiClientSubsystem>();
client?.fetchData('/users');
// The caller only knows the type here, not the concrete class registered above.
for (final notifier in notificationsTypeDesc.allInstancesOfType<NotificationsSubsystem>()) {
notifier.notify('Hello!');
}
// Global, cross-type lookup by capability interface.
for (final participant in SubsystemInstanceRegistry.allInstancesOfClass<BootParticipant>()) {
participant.onBootCompleted();
}
// Predicate-based lookup, e.g. every instance belonging to a specific type category.
final networkInstances = SubsystemInstanceRegistry.allInstancesWhere(
(instance) => instance.classDesc.serviceType.serviceTypeId == 'core.network',
);
print('Found ${networkInstances.length} network subsystem(s).');
// Teardown
print('Tearing down subsystems...');
await SubsystemInstanceRegistry.clearAll();
print('Done!');
}