Overview
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
Basic observer
class StateChangeObserver
implements IPresentumTransitionObserver<CampaignItem, AppSurface, CampaignVariant> {
@override
FutureOr<void> call(PresentumStateTransition transition) {
final diff = transition.diff;
log('State changed at ${transition.timestamp}');
log('Activated: ${diff.activated.length}');
log('Deactivated: ${diff.deactivated.length}');
}
}
Transition structure
Every transition includes:
oldState
PresentumState$Immutable
required
State before the transition
newState
PresentumState$Immutable
required
State after the transition
When the transition occurred
Lazily computed diff between old and new states
The diff property provides:
final diff = transition.diff;
// Items that became active
for (final change in diff.activated) {
print('${change.item.id} activated on ${change.surface}');
}
// Items that became inactive
for (final change in diff.deactivated) {
print('${change.item.id} deactivated from ${change.surface}');
}
// Items added to queues
for (final change in diff.queued) {
print('${change.item.id} queued on ${change.surface}');
}
// Items removed from queues
for (final change in diff.dequeued) {
print('${change.item.id} dequeued from ${change.surface}');
}
// New surfaces that appeared
for (final surface in diff.surfacesAdded) {
print('Surface added: $surface');
}
// Surfaces that were removed
for (final surface in diff.surfacesRemoved) {
print('Surface removed: $surface');
}
Integrating with BLoC
Fire events to your business logic layer:
class BlocTransitionObserver
implements IPresentumTransitionObserver<CampaignItem, AppSurface, CampaignVariant> {
BlocTransitionObserver(this.campaignBloc);
final CampaignBloc campaignBloc;
@override
FutureOr<void> call(PresentumStateTransition transition) {
final diff = transition.diff;
// Fire BLoC events for state changes
for (final change in diff.activated) {
campaignBloc.add(
PresentationActivated(
itemId: change.item.id,
surface: change.surface,
),
);
}
for (final change in diff.deactivated) {
campaignBloc.add(
PresentationDeactivated(
itemId: change.item.id,
surface: change.surface,
),
);
}
for (final change in diff.queued) {
campaignBloc.add(
PresentationQueued(
itemId: change.item.id,
surface: change.surface,
queuePosition: transition.newState.slots[change.surface]!.queue
.indexWhere((item) => item.id == change.item.id),
),
);
}
}
}
Register observers
Add transition observers when creating Presentum:
presentum = Presentum(
storage: storage,
guards: guards,
transitionObservers: [
BlocTransitionObserver(campaignBloc),
LoggingObserver(),
],
);
Conditional data fetching
Fetch additional data when specific presentations become active:
class DataFetchObserver
implements IPresentumTransitionObserver<CampaignItem, AppSurface, CampaignVariant> {
DataFetchObserver(this.dataService);
final DataService dataService;
@override
FutureOr<void> call(PresentumStateTransition transition) async {
for (final change in transition.diff.activated) {
// Fetch product details when product promo becomes active
if (change.item.metadata['type'] == 'product_promo') {
final productId = change.item.metadata['product_id'] as String;
await dataService.prefetchProductDetails(productId);
}
}
}
}
Debug logging
Track state flow in development:
class DebugLoggingObserver
implements IPresentumTransitionObserver<CampaignItem, AppSurface, CampaignVariant> {
@override
FutureOr<void> call(PresentumStateTransition transition) {
final diff = transition.diff;
if (diff.activated.isEmpty &&
diff.deactivated.isEmpty &&
diff.queued.isEmpty) {
return; // No meaningful changes
}
log('β' * 50);
log('State transition at ${transition.timestamp}');
log('β' * 50);
if (diff.activated.isNotEmpty) {
log('β Activated:');
for (final change in diff.activated) {
log(' β’ ${change.item.id} on ${change.surface}');
}
}
if (diff.deactivated.isNotEmpty) {
log('β Deactivated:');
for (final change in diff.deactivated) {
log(' β’ ${change.item.id} from ${change.surface}');
}
}
if (diff.queued.isNotEmpty) {
log('β― Queued:');
for (final change in diff.queued) {
log(' β’ ${change.item.id} on ${change.surface}');
}
}
log('β' * 50);
}
}
Important notes
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.// β Wrong - causes infinite loop
@override
FutureOr<void> call(PresentumStateTransition transition) {
presentum.setState((state) => ...); // Don't do this!
}
// β
Correct - dispatch to business logic
@override
FutureOr<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.
Next steps