Transition observers let you react to state changes in the Presentum engine. Unlike event handlers (which respond to user actions like shown/dismissed/converted), transition observers respond to internal state changes.Each transition includes a comprehensive diff showing exactly what changed between states.
Use transition observers for: - Integrating with BLoC, Provider, Cubit -
Conditional data fetching based on active items - Custom analytics for state
flow - Debug logging of state changes
The diff property provides convenient access to what changed:
Copy
final diff = transition.diff;// Items that became activefor (final item in diff.itemsActivated) { print('${item.id} activated');}// Items that became inactivefor (final item in diff.itemsDeactivated) { print('${item.id} deactivated');}// Items added to queuesfor (final item in diff.itemsQueued) { print('${item.id} queued');}// Items removed from queuesfor (final item in diff.itemsDequeued) { print('${item.id} dequeued');}// New surfaces that appearedfor (final surface in diff.surfacesAdded) { print('Surface added: $surface');}// Surfaces that were removedfor (final surface in diff.surfacesRemoved) { print('Surface removed: $surface');}
For detailed change information including which surface each change occurred on, use pattern matching:
Copy
// Pattern matching for granular changesfor (final change in diff.changes) { change.map( activated: (c) => print('${c.item.id} activated on ${c.surface}'), deactivated: (c) => print('${c.item.id} deactivated from ${c.surface}'), queued: (c) => print('${c.item.id} queued on ${c.surface}'), dequeued: (c) => print('${c.item.id} dequeued from ${c.surface}'), );}// Or handle only specific change typesfor (final change in diff.changes) { change.maybeMap( orElse: () {}, activated: (c) { print('Activated: ${c.item.id} on ${c.surface}'); if (c.previousActive != null) { print(' (replaced ${c.previousActive!.id})'); } }, );}// Quick type checksif (change.isActivated) { print('This is an activation change');}
This observer implements complex business logic (progressive timer backoff,
update status management) without any widget overhead. It’s pure side effects
exactly what transition observers are designed for.
@overrideFutureOr<void> call(PresentumStateTransition transition) { for (final change in transition.diff.changes) { if (change.isActivated) { log('✓ Activated: ${change.item.id} on ${change.surface}'); } else if (change.isDeactivated) { log('✗ Deactivated: ${change.item.id} from ${change.surface}'); } }}
Use transition observers for side effects and business logic: -
Analytics tracking - API calls or data fetching - BLoC/Provider/Cubit
integration - Logging and monitoring - Starting/stopping timers - Any logic
that doesn’t render UI Key indicator: No widget rendering, pure logic
When to use Surface Observers
Use surface observers for UI rendering: -
Showing/hiding snackbars or banners - Triggering widget animations -
Rendering overlays or floating UI - Any visual element that responds to
state Key indicator: Your code renders or manages widgets
If your observer would be a widget with build() returning just
widget.child, use a transition observer instead. You’re adding unnecessary
widget overhead for non-UI concerns.
Do NOT call setState or transaction inside transition observers.This creates circular dependencies and infinite loops. Instead, dispatch events to your business logic layer (BLoC, Provider) which can then coordinate state changes through the public API.
Copy
// ❌ Wrong - causes infinite loop@overrideFutureOr<void> call(PresentumStateTransition transition) { presentum.setState((state) => ...); // Don't do this!}// ✅ Correct - dispatch to business logic@overrideFutureOr<void> call(PresentumStateTransition transition) { bloc.add(StateChanged(transition)); // Business logic decides what to do}
Transition observers run after guards approve but before listeners are notified. They’re synchronous in the state change flow.If an observer throws, the state transition continues. Other observers still run.