orbital_state
Reactive state mixin for OrbitalLifecycle objects in the Orbital ecosystem.
What this package contains
orbital_state provides StateMixin<T>, a small mixin that adds reactive
state to any OrbitalLifecycle controller (as used by orbital_injector and
orbital_router), without requiring an external state manager.
It handles:
- lazily creating the backing notifier on the first
emit - rebuilding widgets via
watchwhenever state changes - subscribing other lifecycle owners via
listen, with automatic cleanup throughaddDisposerwhen the owner is disposed, plus a cancellation handle for stopping earlier - disposing the underlying notifier from
onDispose
The framework does not define loading/error/empty states — model T as you
need (sealed class, enum, plain class, etc.).
Quick start
class CounterController extends OrbitalLifecycle with StateMixin<int> {
@override
FutureOr<void> onInit() {
emit(0);
}
void increment() => emit(state + 1);
}
// In a widget:
controller.watch((count) => Text('count:$count'));
emit drops equal values
emit compares the new value with the current one using ==. When they are
equal it keeps the previously stored instance and notifies nobody: watch does
not rebuild and listen callbacks do not run. The usual trap is a mutable
state object — mutating it in place and re-emitting the same instance is always
a no-op, because an object is equal to itself:
state.items.add(item); // mutation in place
emit(state); // no-op: nothing rebuilds
Prefer immutable state and emit a new value (emit(state.copyWith(...))). When
that is not possible, emit(value, force: true) notifies unconditionally and
stores value even when it compares equal.
watch vs listen
watchreturns a widget that rebuilds itself wheneveremitchanges state.listensubscribes anOrbitalLifecycleowner to another controller's state changes with a plain callback, and automatically removes the subscription when the owner is disposed. PassfireImmediately: trueto invoke the callback once with the current state when registering.
listen returns an OrbitalStateSubscription; call cancel() to stop
listening before the owner is disposed. Cancelling is idempotent. Registering
the same owner and callback again is a no-op that returns the existing handle,
so listeners and disposers never stack — but note that two closure literals are
never equal, so pass a method tear-off (this.onValue) when deduplication
matters.
Both require at least one prior emit — emit the initial state in onInit
before calling watch or listen. Violating that throws
OrbitalStateNotEmittedException, in release builds too.
After disposal
Once the owning controller has been disposed, the state API degrades safely:
emitis a no-op (even withforce), so an in-flight async callback that resolves after the page was popped cannot touch a disposed notifierwatchrenders the builder once with the last known state instead of attaching a listenerlistenreturns an already-cancelled handle: no listener is attached and no disposer is registered, even withfireImmediatelyonDisposeis idempotent — calling it twice (or having both the owning container and a caller dispose) disposes the notifier exactly once, and disposing the subject from inside one of its own listeners during a notification round is deferred safely instead of tripping aChangeNotifierassertion
More detail
See doc.md for the design rationale and lifecycle interaction with
orbital_core and orbital_injector.