orbital_state 0.3.0
orbital_state: ^0.3.0 copied to clipboard
Reactive state mixin and UI helpers for Orbital lifecycle objects.
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.