orchestra_generator 1.0.1
orchestra_generator: ^1.0.1 copied to clipboard
build_runner code generator for Orchestra. Eliminates boilerplate by generating entity and system classes from the Composer declarative DSL.
Orchestra Generator #
A build_runner code generator for the orchestra package. Write your orchestrations declaratively using plain Dart and let the generator produce all the boilerplate class definitions automatically.
Table of Contents #
- Overview
- How It Works
- Installation
- Setup
- Quick Start
- Entity Types
- System Types
- Guard Conditions
- Helper Functions
- Cross-File Orchestrations
- Generated Output
- Running the Generator
- Rules & Constraints
Overview #
The orchestra_generator package analyses your orchestra declaration files using the Dart analyzer and generates fully-typed, boilerplate-free orchestration classes. All entity variables, component mutations, event triggers, and system logic you write declaratively are automatically translated into proper class definitions that extend the base classes from orchestra.
Instead of writing:
// Manually written boilerplate
final class IsLoadingComponent extends Component<bool> {
IsLoadingComponent() : super(false);
}
final class DataFetchedEvent extends Event {}
final class LoadingReactiveSystem extends ReactiveSystem {
@override
Set get reactsTo {
return const {DataFetchedEvent};
}
@override
Set get interactsWith {
return const {IsLoadingComponent};
}
@override
void react() {
get<IsLoadingComponent>().value = false;
}
}
final class AuthFeatureOrchestration extends Orchestration {
AuthFeatureOrchestration() {
add(IsLoadingComponent());
add(DataFetchedEvent());
add(LoadingReactiveSystem());
}
}
You simply write:
// Declarative definition — generator handles everything else
final authFeature = Composer.createOrchestration();
final isLoading = authFeature.addComponent(false);
final dataFetched = authFeature.addEvent();
final onDataFetched = authFeature.addReactiveSystem(
reactsTo: {dataFetched},
react: () {
isLoading.value = false;
},
);
How It Works #
The generator runs as a build_runner builder and processes each .dart file in your project:
- Feature discovery —
OrchestrationVisitorfinds everyComposer.createOrchestration()call and registers it. - Entity discovery —
EntityVisitorfinds alladdComponent,addEvent,addDataEvent, andaddDependencycalls attached to a known feature. - System discovery —
SystemVisitorfinds alladdReactiveSystem,addExecuteSystem,addCleanupSystem,addTeardownSystem, andaddInitializeSystemcalls. - AST rewriting — All expression bodies are rewritten using the Dart AST, replacing local variable references with proper
get<Type>()calls. interactsWithinference — The generator automatically detects which entities each system mutates (via.value =,.update(), or.trigger()) and populates theinteractsWithset.- Code generation — A formatted
.g.dartpartfile is written alongside each source file.
Installation #
Add both the runtime package and this generator to your pubspec.yaml:
dependencies:
orchestra: ^<latest_version>
dev_dependencies:
orchestra_generator: ^<latest_version>
build_runner: ^<compatible_version>
Setup #
1. Register the builder #
Create a build.yaml file at the root of your project:
targets:
$default:
builders:
orchestra_generator:
enabled: true
2. Add the part directive #
In every file where you declare a feature, add a part directive pointing to the generated file:
// my_feature.dart
part 'my_feature.g.dart';
Quick Start #
// user_profile_feature.dart
import 'package:orchestra/orchestra.dart';
part 'user_profile_feature.g.dart';
// 1. Create a feature (one per file)
final userProfileFeature = Composer.createOrchestration();
// 2. Declare entities
final username = userProfileFeature.addComponent('');
final isLoggedIn = userProfileFeature.addComponent(false);
final loggedIn = userProfileFeature.addEvent();
final errorOccurred = userProfileFeature.addDataEvent<String>();
// 3. Declare systems
final onLogin = userProfileFeature.addReactiveSystem(
reactsTo: {loggedIn},
react: () {
isLoggedIn.value = true;
},
);
final onError = userProfileFeature.addReactiveSystem(
reactsTo: {errorOccurred},
react: () {
isLoggedIn.value = false;
_logError(errorOccurred.data);
},
);
void _logError(String message) {
// handle error
}
final sessionTimer = userProfileFeature.addExecuteSystem(
executesIf: () {
return isLoggedIn.value;
},
execute: (Duration elapsed) {
sessionDuration.value += elapsed.inSeconds;
},
);
Run the generator (see Running the Generator) and user_profile_feature.g.dart will contain all the concrete class definitions.
Entity Types #
| Method | Generated Base Class | Description |
|---|---|---|
addComponent<T>(defaultValue) |
Component<T> |
Stateful data holder with .value, .previous, .updatedAt |
addEvent() |
Event |
Signal with no payload; carries a .triggeredAt timestamp |
addDataEvent<T>() |
DataEvent<T> |
Signal that carries a typed .data payload |
addDependency<T>(value) |
Dependency<T> |
Externally injected value, accessible across systems |
Component #
final pageIndex = myFeature.addComponent(0);
final searchQuery = myFeature.addComponent('');
final currentUser = myFeature.addComponent<User?>(null);
Accessing component state in a system body:
react: () {
pageIndex.value; // current value
pageIndex.previous; // value before last update
pageIndex.updatedAt; // DateTime of last update
pageIndex.value = 2; // direct assignment
pageIndex.update(2, notify: true, force: false); // explicit update with options
},
Events #
final formSubmitted = myFeature.addEvent(); // no payload
final userSelected = myFeature.addDataEvent<String>(); // carries a String id
// In a system body:
react: () {
formSubmitted.trigger(); // fire the event
userSelected.trigger('user-123'); // fire with payload
userSelected.data; // read the payload
userSelected.triggeredAt; // DateTime last triggered
},
Dependency #
final apiService = myFeature.addDependency<ApiService>(ApiService());
System Types #
Reactive System #
Triggers in response to one or more entities changing (component updated or event fired).
final onSubmit = myFeature.addReactiveSystem(
reactsTo: {formSubmitted},
react: () {
isLoading.value = true;
submitForm.trigger();
},
);
Execute System #
Runs every tick/frame. Receives the Duration elapsed since the last tick.
final countdownSystem = myFeature.addExecuteSystem(
execute: (Duration elapsed) {
timeRemaining.value -= elapsed.inSeconds;
},
);
Cleanup System #
Runs to reset or flush transient per-frame state.
final clearNotifications = myFeature.addCleanupSystem(
cleanup: () {
pendingNotificationCount.value = 0;
},
);
Initialize System #
Runs once when the feature is first initialized.
final setup = myFeature.addInitializeSystem(
initialize: () {
pageIndex.value = 0;
isLoading.value = false;
},
);
Teardown System #
Runs when the feature is being disposed.
final dispose = myFeature.addTeardownSystem(
teardown: () {
currentUser.value = null;
isLoggedIn.value = false;
},
);
Guard Conditions #
Every system type (except teardown) supports an optional guard. The system only executes when the guard returns true.
| System | Guard Parameter |
|---|---|
addReactiveSystem |
reactsIf |
addExecuteSystem |
executesIf |
addCleanupSystem |
cleansIf |
final onUserSelected = myFeature.addReactiveSystem(
reactsTo: {userSelected},
reactsIf: () {
return isLoggedIn.value;
}, // only react when authenticated
react: () {
activeUserId.value = userSelected.data;
},
);
final countdownSystem = myFeature.addExecuteSystem(
executesIf: () {
return timeRemaining.value > 0;
}, // stop when timer reaches zero
execute: (Duration elapsed) {
timeRemaining.value -= elapsed.inSeconds;
},
);
Helper Functions #
You can call top-level helper functions from within system bodies. The generator resolves them automatically — including transitive call chains — and rewrites all entity references before inlining them into the generated class.
final onSubmit = myFeature.addReactiveSystem(
reactsTo: {formSubmitted},
react: () {
_processForm();
},
);
void _processForm() {
final sanitized = _sanitizeInput(inputText.value);
inputText.value = sanitized;
}
String _sanitizeInput(String raw) {
return raw.trim().toLowerCase();
}
The generator walks the call graph from _processForm, discovers _sanitizeInput, rewrites all entity accesses (e.g. inputText → get<InputTextComponent>()), and includes both helpers in the generated system class.
Cross-File Orchestrations #
Entities declared in imported files are fully resolved. When feature B imports feature A, the generator scans all transitive imports and resolves entity references across file boundaries.
// session_feature.dart
import 'package:orchestra/orchestra.dart';
part 'session_feature.g.dart';
final sessionFeature = Composer.createOrchestration();
final authToken = sessionFeature.addComponent<String?>(null);
// settings_feature.dart
import 'package:orchestra/orchestra.dart';
import 'session_feature.dart';
part 'settings_feature.g.dart';
final settingsFeature = Composer.createOrchestration();
final settingsSaved = settingsFeature.addEvent();
final onSettingsSaved = settingsFeature.addReactiveSystem(
reactsTo: {settingsSaved},
react: () {
// cross-file entity reference — fully supported
if (authToken.value == null) return;
syncSettings.trigger();
},
);
Generated Output #
For the quick-start example, the generator produces user_profile_feature.g.dart:
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_profile_feature.dart';
final class UsernameComponent extends Component<String> {
UsernameComponent() : super('');
}
final class IsLoggedInComponent extends Component<bool> {
IsLoggedInComponent() : super(false);
}
final class LoggedInEvent extends Event {}
final class ErrorOccurredEvent extends DataEvent<String> {}
final class OnLoginReactiveSystem extends ReactiveSystem {
@override
Set get reactsTo {
return const {LoggedInEvent};
}
@override
Set get interactsWith {
return const {IsLoggedInComponent};
}
@override
void react() {
get<IsLoggedInComponent>().value = true;
}
}
final class OnErrorReactiveSystem extends ReactiveSystem {
@override
Set get reactsTo {
return const {ErrorOccurredEvent};
}
@override
Set get interactsWith {
return const {IsLoggedInComponent};
}
@override
void react() {
get<IsLoggedInComponent>().value = false;
_logError(get<ErrorOccurredEvent>().data);
}
void _logError(String message) {
// handle error
}
}
final class UserProfileFeatureOrchestration extends Orchestration {
UserProfileFeatureOrchestration() {
add(UsernameComponent());
add(IsLoggedInComponent());
add(LoggedInEvent());
add(ErrorOccurredEvent());
add(OnLoginReactiveSystem());
add(OnErrorReactiveSystem());
}
}
Key things to note in the generated output:
- All local variable references (e.g.
isLoggedIn,errorOccurred) are rewritten toget<Type>()calls. - The
interactsWithset is automatically inferred — you never write it manually. - Guard conditions become getter overrides on the generated class.
- Helper functions are resolved, rewritten, and inlined into the system class.
- The feature class registers all entities and systems in its constructor.
Running the Generator #
One-time build:
dart run build_runner build
Watch mode (re-generates on every file save):
dart run build_runner watch
Clean conflicting outputs first:
dart run build_runner build --delete-conflicting-outputs
Generated .g.dart files are deterministic and should be committed to version control.
Rules & Constraints #
- One orchestration per file — each
.dartfile must contain exactly oneComposer.createOrchestration()call. Files with more than one orchestration log a warning and produce no output. - Function body required —
react,execute,cleanup,initialize,teardown, and guard parameters must be function body expressions, not references to named functions and not lambdas. - Top-level declarations only — orchestrations, entities, and systems must be top-level variables, not members of a class or locals inside a function.
partdirective required — the source file must includepart '<filename>.g.dart';to use the generated code.- Auto-naming — missing suffixes are appended automatically:
isLoading→IsLoadingComponent,formSubmitted→FormSubmittedEvent,onLogin→OnLoginReactiveSystem.