dart_subsystem
A robust, lifecycle-aware framework for managing singleton services (subsystems) in Dart and Flutter applications.
Inspired by advanced modular architectures (such as Unreal Engine Subsystems), this package provides a highly structured, scalable, and fail-safe way to define, initialize, access, and dispose of global or scoped services.
Features
- Explicit Lifecycle Hooks: Predictable states (
isConstructed,isInitialized,isDisposed) with guaranteed sequential execution. - Dependency & Async Awareness: Supports both synchronous foundational services and asynchronous parallel services during startup.
- Fail-Safe Initialization: If a subsystem fails to initialize, it is safely rolled back and disposed, preventing "zombie" singletons from polluting your app state.
- Idempotent Teardown: Ensures resources are cleaned up cleanly. A
finalDispose()hook runs reliably even if primary disposal throws an exception. - Type-Safe Organization: Group implementations under category types (
SubsystemTypeDesc) and concrete definitions (SubsystemClassDesc) for clean abstraction and lookup.
Installation
Add the following to your pubspec.yaml:
dependencies:
dart_subsystem: ^0.1.3
Quick Start
1. Define Subsystem Descriptors
Subsystems are organized by types and specific class implementations.
import 'package:dart_subsystem/dart_subsystem.dart';
// 1. Define the Category/Type
final networkTypeDesc = SubsystemTypeDesc(
displayName: 'Network Services',
description: 'Handles all external communications',
serviceTypeId: 'core.network',
);
// 2. Define the Concrete Implementation Descriptor
final apiClassDesc = SubsystemClassDesc(
serviceType: networkTypeDesc,
displayName: 'REST API Subsystem',
description: 'Manages REST API calls',
serviceClassId: 'network.api',
defaultBuilder: (params) async => ApiSubsystem(classDesc: params.classDescription),
);
2. Implement your Subsystem
Extend the Subsystem class and override the lifecycle hooks you need.
class ApiSubsystem extends Subsystem {
ApiSubsystem({required super.classDesc});
@override
void beginConstruct(BuildServiceParameters params) {
// Synchronous setup: runs immediately after instantiation
print('Constructing API Subsystem...');
}
@override
Future<void> initialize(BuildServiceParameters params) async {
// Asynchronous setup: connect to databases, fetch tokens, etc.
print('Initializing API connections...');
await Future.delayed(Duration(seconds: 1));
}
@override
Future<void> dispose() async {
// Clean up resources when the app shuts down or service is removed
print('Closing API connections...');
}
}
3. Register and Build
Use the TSubsystemFactory to queue up your services and build them safely.
void main() async {
final factory = TSubsystemFactory();
// Add the service. You can pass 'isSynchronous: true' if other services depend on it immediately.
factory.addService(BuildServiceParameters(classDescription: apiClassDesc));
try {
// Builds and initializes all registered subsystems
await factory.registerSubsystems();
print('All subsystems are ready!');
} on SubsystemInitializationException catch (e) {
print('Startup failed: ${e.message}');
}
}
4. Access your Subsystem
Once initialized, you can retrieve your subsystem anywhere in your app synchronously.
// Synchronous retrieval via registry (throws SubsystemNotFoundException if missing)
final api = SubsystemInstanceRegistry.findSubsystemByIdsChecked<ApiSubsystem>(
'core.network',
'network.api',
);
// Or safe retrieval (returns null if missing)
final maybeApi = SubsystemInstanceRegistry.findSubsystemByIds<ApiSubsystem>(
'core.network',
'network.api',
);
// Or directly via the descriptor
final api = apiClassDesc.getInstance<ApiSubsystem>();
Lifecycle Phases
- Construction:
beginConstruct->postConstruct(Synchronous) - Initialization:
initialize->postInitialize(Asynchronous) - Teardown:
dispose->finalDispose(Asynchronous, guaranteed execution)
Subclasses can call assertNotDisposed() at the entry of public methods to prevent usage after disposal.
Error Handling
If a subsystem throws an exception during initialize() or postInitialize(), dart_subsystem handles it gracefully:
- The failing subsystem is immediately removed from the registry.
- Its
dispose()andfinalDispose()methods are called to clean up any partial state. - The original exception is bundled into a
SubsystemInitializationExceptionand rethrown. - You can inspect failures via
SubsystemInstanceRegistry.lastFailedRegistrations. - Pass
atomicBatch: truetoregisterSubsystems(atomicBatch: true)if you want an all-or-nothing rollback when any subsystem fails in a batch. - Configure
SubsystemInstanceRegistry.onLifecycleErrorto log any non-fatal errors that occur during disposal without crashing teardown pipelines.
Requirements
Subsystems can declare dependencies and incompatibilities through requirements. They are checked at registration, before the type or class is initialized:
final syncClassDesc = SubsystemClassDesc(
serviceType: networkTypeDesc,
displayName: 'Sync Subsystem',
description: 'Synchronizes data through the API client',
serviceClassId: 'network.sync',
requirements: [SubsystemRequiresClass.fromDesc(apiClassDesc)],
defaultBuilder: (params) async => SyncSubsystem(classDesc: params.classDescription),
);
Available constraints:
SubsystemRequiresType(typeId): at least one active subsystem of this type.SubsystemRequiresClass({typeId, classId})orSubsystemRequiresClass.fromDesc(desc): a specific subsystem must be registered.SubsystemIncompatibleWithType(typeId): no active subsystem of this type.SubsystemIncompatibleWithClass({typeId, classId})orSubsystemIncompatibleWithClass.fromDesc(desc): a specific subsystem must not be registered.
A violation throws SubsystemRequirementException, wrapped in SubsystemInitializationException by registerSubsystems(). Checks depend on order: a required subsystem must be registered earlier in the batch.
License
This project is licensed under the MIT License - see the LICENSE file for details.