orchestra_flutter
Flutter widget integration for orchestra. Provides the scope, reactive widgets, and handle API that connect the Orchestra ECS state management layer to the Flutter widget tree.
Packages
| Package | Description |
|---|---|
orchestra |
Core ECS runtime — entities, systems, orchestrations |
orchestra_flutter |
Flutter widget integration — scope, reactive widgets, handle |
orchestra_generator |
build_runner code generator for the Composer DSL |
Installation
dependencies:
orchestra: ^1.0.0
orchestra_flutter: ^1.0.0
Setup
Wrap your app (or a subtree) with OrchestraScope, providing the orchestrations that should be active for that subtree:
import 'package:orchestra_flutter/orchestra_flutter.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return OrchestraScope(
orchestrations: {
CounterOrchestration(),
AuthOrchestration(),
},
child: MaterialApp(
home: HomePage(),
),
);
}
}
OrchestraScope creates an Orchestrator, initializes all orchestrations, and tears everything down when the widget is removed from the tree. If any orchestration contains ExecuteSystem or CleanupSystem instances, a Ticker is started automatically to drive them each frame.
The optional name parameter is useful for debugging and the Inspector DevTools extension:
OrchestraScope(
name: 'MainScope',
orchestrations: { ... },
child: ...,
)
Reactive Widgets
OrchestraWidget
Extend OrchestraWidget for stateless-style widgets that rebuild automatically when watched entities change. The build method receives an OrchestraHandle alongside the standard BuildContext.
class CounterPage extends OrchestraWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, OrchestraHandle handle) {
final counter = handle.watch<CounterComponent>();
final increment = handle.get<IncrementEvent>();
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Text('${counter.value}'),
),
floatingActionButton: FloatingActionButton(
onPressed: increment.trigger,
child: const Icon(Icons.add),
),
);
}
}
OrchestraBuilder
Functional alternative — useful for scoping reactivity to a small part of an existing widget tree without creating a new class.
OrchestraBuilder(
builder: (BuildContext context, OrchestraHandle handle) {
final counter = handle.watch<CounterComponent>();
return Text('${counter.value}');
},
)
OrchestraStatefulWidget
For widgets that need both Orchestra access and local State. Access the handle via the orchestra getter on the state.
class SearchPage extends OrchestraStatefulWidget {
const SearchPage({super.key});
@override
OrchestraState<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends OrchestraState<SearchPage> {
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
final results = orchestra.watch<SearchResultsComponent>();
return Column(
children: [
TextField(controller: _controller),
Expanded(child: ResultsList(results.value)),
],
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
OrchestraHandle
Every reactive widget receives an OrchestraHandle. It provides three ways to interact with entities:
get<T>() — read without subscribing
Retrieves an entity without subscribing. The widget does not rebuild when this entity changes. Use for write-only access (e.g. triggering events).
final increment = handle.get<IncrementEvent>();
ElevatedButton(onPressed: increment.trigger, child: const Text('Add'))
watch<T>() — read and subscribe
Retrieves an entity and subscribes to it. The widget rebuilds automatically when the entity changes.
final counter = handle.watch<CounterComponent>();
Text('${counter.value}') // re-renders on every counter change
listen<T>(callback) — side effects without rebuilding
Subscribes to an entity and runs a callback instead of rebuilding. Useful for showing snackbars, navigating, or any side effect in response to a state change.
Callbacks are executed at the next frame boundary and batched with other listeners.
handle.listen<ErrorComponent>((error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error.value.message)),
);
});
Multiple calls to
listenwith the same entity type override the previous listener — only the most recent callback is kept.
Lifecycle Callbacks
onEnter
Called once at the next frame boundary after the widget is first built. Safe to trigger events or mutate components here.
handle.onEnter(() {
handle.get<LoadDataEvent>().trigger();
});
onExit
Called synchronously when the widget is disposed. Entities are still accessible.
handle.onExit(() {
handle.get<CancelRequestEvent>().trigger();
});
Both are one-shot — subsequent calls within the same widget instance are silently ignored.
Full Example
// orchestration
class TodoOrchestration extends Orchestration {
TodoOrchestration() {
add(TodoListComponent());
add(LoadTodosEvent());
add(AddTodoEvent()); // DataEvent<String>
add(LoadTodosSystem());
add(AddTodoSystem());
}
}
// app setup
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return OrchestraScope(
orchestrations: {TodoOrchestration()},
child: MaterialApp(home: TodoPage()),
);
}
}
// reactive widget
class TodoPage extends OrchestraWidget {
const TodoPage({super.key});
@override
Widget build(BuildContext context, OrchestraHandle handle) {
final todos = handle.watch<TodoListComponent>();
final addTodo = handle.get<AddTodoEvent>();
final loadTodos = handle.get<LoadTodosEvent>();
handle.onEnter(() => loadTodos.trigger());
handle.listen<TodoListComponent>((_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('List updated')),
);
});
return Scaffold(
appBar: AppBar(title: const Text('Todos')),
body: ListView(
children: todos.value.map((t) => ListTile(title: Text(t))).toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => addTodo.trigger('New todo'),
child: const Icon(Icons.add),
),
);
}
}
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