orchestra 1.0.2
orchestra: ^1.0.2 copied to clipboard
Reactive state management for Dart and Flutter. Clean architecture, deterministic logic, and built-in DevTools Inspector.
Orchestra #
A reactive state management library for Dart and Flutter. Promotes clean architecture, deterministic business logic, and scalable application design through strict separation of data, behavior, and events.
Why Orchestra #
| Concern | Traditional approach | Orchestra |
|---|---|---|
| State | Scattered across notifiers / blocs | Typed Component<T> entities |
| Logic | Coupled to widgets or providers | Stateless System classes |
| Events | Callbacks / streams passed through the tree | First-class Event / DataEvent entities |
| Modules | Global providers or nested scopes | Self-contained Orchestration |
| Debugging | Ad-hoc logging | Built-in Inspector DevTools extension |
Core Concepts #
Entities #
Everything that holds data or represents an occurrence is an entity.
| Type | Description |
|---|---|
Component<T> |
Holds mutable state. Notifies listeners on change. |
Event |
A trigger with no payload. |
DataEvent<T> |
A trigger that carries typed data. |
Dependency<T> |
An immutable value injected into systems. |
Systems #
Systems contain all business logic. They are stateless and operate on entities.
| Type | When it runs |
|---|---|
InitializeSystem |
Once, after the orchestration activates |
ReactiveSystem |
When a watched entity changes or an event fires |
ExecuteSystem |
Every frame (ticker-driven) |
CleanupSystem |
After every execute pass |
TeardownSystem |
Once, when the orchestration is deactivated |
Orchestration #
Groups related entities and systems into a cohesive, self-contained module.
Orchestrator #
Central coordinator. Manages one or more orchestrations, drives system execution, and provides type-based entity lookup across the whole graph.
Installation #
dependencies:
orchestra: ^1.0.0
# Optional: add if you want to use the Composer declarative API
dev_dependencies:
orchestra_generator: ^1.0.0
build_runner: ^2.0.0
Quick Start #
There are two ways to use Orchestra:
- Manual — subclass
Component,Event,System, andOrchestrationdirectly (shown below). - Code generation — declare everything with
Composerand letorchestra_generatorgenerate the classes. See Code Generation.
1. Define entities #
// Component — holds state
class CounterComponent extends Component<int> {
CounterComponent() : super(0);
}
// Event — parameterless trigger
class IncrementEvent extends Event {}
// DataEvent — trigger with payload
class SetCounterEvent extends DataEvent<int> {}
2. Write systems #
class IncrementSystem extends ReactiveSystem {
@override
Set<Type> get reactsTo => {IncrementEvent};
@override
Set<Type> get interactsWith => {CounterComponent};
@override
void react() {
final counter = get<CounterComponent>();
counter.update(counter.value + 1);
}
}
class SetCounterSystem extends ReactiveSystem {
@override
Set<Type> get reactsTo => {SetCounterEvent};
@override
Set<Type> get interactsWith => {CounterComponent};
@override
void react() {
final event = get<SetCounterEvent>();
final counter = get<CounterComponent>();
counter.update(event.data);
}
}
3. Bundle into an orchestration #
class CounterOrchestration extends Orchestration {
CounterOrchestration() {
add(CounterComponent());
add(IncrementEvent());
add(SetCounterEvent());
add(IncrementSystem());
add(SetCounterSystem());
}
}
4. Wire up the orchestrator #
final orchestrator = Orchestrator(
orchestrations: {CounterOrchestration()},
);
// Trigger events
orchestrator.get<IncrementEvent>().trigger();
orchestrator.get<SetCounterEvent>().trigger(42);
// Read state
print(orchestrator.get<CounterComponent>().value); // 42 + 1 = 43
Entity API #
Component #
final counter = orchestrator.get<CounterComponent>();
counter.update(10); // update + notify listeners
counter.update(10, notify: false); // silent update
counter.update(10, force: true); // update even if value is equal
counter.value = 20; // shorthand for update(20)
print(counter.value); // current value
print(counter.previous); // previous value after last update
print(counter.updatedAt); // DateTime of last update
Event #
final increment = orchestrator.get<IncrementEvent>();
increment.trigger();
print(increment.triggeredAt); // DateTime of last trigger
DataEvent #
final setCounter = orchestrator.get<SetCounterEvent>();
setCounter.trigger(99);
print(setCounter.data); // 99 — throws if never triggered
print(setCounter.dataOrNull); // 99? — null if never triggered
print(setCounter.triggeredAt);
Dependency #
// Define
class ApiUrlDependency extends Dependency<String> {
ApiUrlDependency(super.value);
}
// Register in an orchestration
add(ApiUrlDependency('https://api.example.com'));
// Access in a system
final url = get<ApiUrlDependency>().value;
System API #
ReactiveSystem #
class UpdateCacheSystem extends ReactiveSystem {
@override
Set<Type> get reactsTo => {UserComponent, AuthEvent};
@override
Set<Type> get interactsWith => {CacheComponent};
// Optional guard — skip react() when false
@override
bool get reactsIf => get<AuthComponent>().value.isLoggedIn;
@override
void react() {
// called once per qualifying entity change
}
}
ExecuteSystem #
class AnimationSystem extends ExecuteSystem {
@override
Set<Type> get interactsWith => {AnimationComponent};
@override
bool get executesIf => get<AnimationComponent>().value.isPlaying;
@override
void execute(Duration elapsed) {
final anim = get<AnimationComponent>();
anim.update(anim.value.advance(elapsed));
}
}
InitializeSystem / TeardownSystem #
class DatabaseInitSystem extends InitializeSystem {
@override
Set<Type> get interactsWith => {DatabaseComponent};
@override
void initialize() {
get<DatabaseComponent>().update(Database.open());
}
}
class DatabaseTeardownSystem extends TeardownSystem {
@override
void teardown() {
get<DatabaseComponent>().value.close();
}
}
Logging from systems and entities #
// Inside any System
log('Processing user data');
log('Entity null — skipping', level: LogLevel.warning);
// Inside any Entity
log('Value sanitized before update');
Code Generation (Recommended) #
Orchestra includes a declarative API via Composer that eliminates boilerplate. Instead of writing entity and system classes by hand, you declare your orchestration inline and let orchestra_generator generate all the class definitions at build time.
Composeris a code-generation DSL, not a runtime API. It requiresorchestra_generatorandbuild_runnerto produce the actual Dart classes. See the orchestra_generator README for full documentation.
// counter_feature.dart
import 'package:orchestra/orchestra.dart';
part 'counter_feature.g.dart'; // generated output
final counterFeature = Composer.createOrchestration();
final counter = counterFeature.addComponent(0);
final increment = counterFeature.addEvent();
final setTo = counterFeature.addDataEvent<int>();
final onIncrement = counterFeature.addReactiveSystem(
reactsTo: {increment},
react: () {
counter.value = counter.value + 1;
},
);
final onSetTo = counterFeature.addReactiveSystem(
reactsTo: {setTo},
react: () {
counter.value = setTo.data;
},
);
Run the generator, and counter_feature.g.dart is produced with all entity classes, system classes, and the CounterFeatureOrchestration wired up automatically.
dart run build_runner build
Orchestration Lifecycle #

Testing #
Unit-testing a system #
test('IncrementSystem increments counter', () {
final orchestration = CounterOrchestration();
final orchestrator = Orchestrator(orchestrations: {orchestration});
orchestrator.get<IncrementEvent>().trigger();
expect(orchestrator.get<CounterComponent>().value, 1);
});
Testing reactive behavior #
test('system reacts to event', () {
final orchestration = CounterOrchestration();
final orchestrator = Orchestrator(orchestrations: {orchestration});
orchestrator.initialize();
orchestrator.get<SetCounterEvent>().trigger(55);
expect(orchestrator.get<CounterComponent>().value, 55);
});
Testing components in isolation #
test('component tracks previous value', () {
final component = CounterComponent();
component.update(10);
component.update(20);
expect(component.value, 20);
expect(component.previous, 10);
});
test('component skips equal values', () {
final component = CounterComponent();
int notifyCount = 0;
component.addListener(TestListener(() => notifyCount++));
component.update(0); // equal — skipped
expect(notifyCount, 0);
});
Inspector DevTools Extension #
Orchestra ships with a built-in DevTools extension. When running in debug mode, open Flutter DevTools and navigate to the Orchestra tab to inspect:
- All active orchestrators and orchestrations
- Live entity state and change history
- System execution logs and timing
- Reactive dependency graph
No setup required — the extension registers automatically on first Orchestrator construction.
License #
Apache License 2.0 — see the LICENSE file for details.
Copyright 2026 Ehsan Rashidi
Issues: github.com/FlameOfUdun/orchestra/issues Discussions: github.com/FlameOfUdun/orchestra/discussions