> ## Documentation Index
> Fetch the complete documentation index at: https://docs.presentum.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Transition observers

> React to state changes with comprehensive diff snapshots

## 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.

<Info>
  **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
</Info>

## Basic observer

```dart theme={null}
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.itemsActivated.length}');
    log('Deactivated: ${diff.itemsDeactivated.length}');
  }
}
```

## Transition structure

Every transition includes:

<ParamField path="oldState" type="PresentumState$Immutable" required>
  State before the transition
</ParamField>

<ParamField path="newState" type="PresentumState$Immutable" required>
  State after the transition
</ParamField>

<ParamField path="timestamp" type="DateTime" required>
  When the transition occurred
</ParamField>

<ParamField path="diff" type="PresentumStateDiff">
  Lazily computed diff between old and new states
</ParamField>

## Diff information

The `diff` property provides convenient access to what changed:

```dart theme={null}
final diff = transition.diff;

// Items that became active
for (final item in diff.itemsActivated) {
  print('${item.id} activated');
}

// Items that became inactive
for (final item in diff.itemsDeactivated) {
  print('${item.id} deactivated');
}

// Items added to queues
for (final item in diff.itemsQueued) {
  print('${item.id} queued');
}

// Items removed from queues
for (final item in diff.itemsDequeued) {
  print('${item.id} dequeued');
}

// 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');
}
```

For detailed change information including which surface each change occurred on, use pattern matching:

```dart theme={null}
// Pattern matching for granular changes
for (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 types
for (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 checks
if (change.isActivated) {
  print('This is an activation change');
}
```

## Integrating with BLoC

Fire events to your business logic layer using pattern matching:

```dart theme={null}
class BlocTransitionObserver
    implements IPresentumTransitionObserver<CampaignItem, AppSurface, CampaignVariant> {
  BlocTransitionObserver(this.campaignBloc);

  final CampaignBloc campaignBloc;

  @override
  FutureOr<void> call(PresentumStateTransition transition) {
    // Use pattern matching for clean, exhaustive handling
    for (final change in transition.diff.changes) {
      change.map(
        activated: (c) => campaignBloc.add(
          PresentationActivated(
            itemId: c.item.id,
            surface: c.surface,
            previousId: c.previousActive?.id,
          ),
        ),
        deactivated: (c) => campaignBloc.add(
          PresentationDeactivated(
            itemId: c.item.id,
            surface: c.surface,
          ),
        ),
        queued: (c) => campaignBloc.add(
          PresentationQueued(
            itemId: c.item.id,
            surface: c.surface,
          ),
        ),
        dequeued: (c) => campaignBloc.add(
          PresentationDequeued(
            itemId: c.item.id,
            surface: c.surface,
          ),
        ),
      );
    }
  }
}
```

Or handle only specific change types with `maybeMap`:

```dart theme={null}
@override
FutureOr<void> call(PresentumStateTransition transition) {
  for (final change in transition.diff.changes) {
    change.maybeMap(
      orElse: () {}, // Ignore other change types
      activated: (c) => campaignBloc.add(
        PresentationActivated(
          itemId: c.item.id,
          surface: c.surface,
        ),
      ),
      deactivated: (c) => campaignBloc.add(
        PresentationDeactivated(
          itemId: c.item.id,
          surface: c.surface,
        ),
      ),
    );
  }
}
```

## Register observers

Add transition observers when creating Presentum:

```dart theme={null}
presentum = Presentum(
  storage: storage,
  guards: guards,
  transitionObservers: [
    BlocTransitionObserver(campaignBloc),
    LoggingObserver(),
  ],
);
```

## Conditional data fetching

Fetch additional data when specific presentations become active:

```dart theme={null}
class DataFetchObserver
    implements IPresentumTransitionObserver<CampaignItem, AppSurface, CampaignVariant> {
  DataFetchObserver(this.dataService);

  final DataService dataService;

  @override
  FutureOr<void> call(PresentumStateTransition transition) async {
    // Simple case: just check activated items
    for (final item in transition.diff.itemsActivated) {
      if (item.metadata['type'] == 'product_promo') {
        final productId = item.metadata['product_id'] as String;
        await dataService.prefetchProductDetails(productId);
      }
    }
  }
}
```

## Production example: Maintenance mode observer

This real-world observer from the example app manages app update checks based on maintenance mode:

```dart theme={null}
class MaintenanceTransitionObserver
    implements IPresentumTransitionObserver<
      MaintenanceItem,
      AppSurface,
      AppVariant
    > {

  MaintenanceTransitionObserver(this.updatesStore);

  final ShorebirdUpdatesStore updatesStore;
  Timer? _updatesCheckTimer;
  int _checkInterval = 5; // Start with 5 seconds

  @override
  FutureOr<void> call(PresentumStateTransition transition) {
    final diff = transition.diff;

    // Check if maintenance surface was deactivated
    final maintenanceDeactivated = diff.itemsDeactivated.any(
      (change) => change.surface == AppSurface.maintenanceView,
    );

    if (maintenanceDeactivated) {
      _startUpdateChecks();
      return;
    }

    // Check if maintenance surface was activated
    final maintenanceActivated = diff.itemsActivated.any(
      (change) => change.surface == AppSurface.maintenanceView,
    );

    if (maintenanceActivated) {
      _stopUpdateChecks();
    }
  }

  ... callbacks implementations
}
```

Register it when creating the Presentum instance:

```dart theme={null}
maintenancePresentum = Presentum(
  storage: storage,
  guards: guards,
  transitionObservers: [
    MaintenanceTransitionObserver(updatesStore),
  ],
);
```

[See full source →](https://github.com/itsezlife/presentum/blob/master/example/lib/src/maintenance/presentum/maintenance_transition_observer.dart)

<Info>
  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.
</Info>

## Debug logging

Track state flow in development with pattern matching:

```dart theme={null}
class DebugLoggingObserver
    implements IPresentumTransitionObserver<CampaignItem, AppSurface, CampaignVariant> {

  @override
  FutureOr<void> call(PresentumStateTransition transition) {
    final diff = transition.diff;

    if (diff.isEmpty) return;

    log('═' * 50);
    log('State transition at ${transition.timestamp}');
    log('─' * 50);

    // Use pattern matching for detailed logging
    for (final change in diff.changes) {
      final emoji = change.map(
        activated: (_) => '✓',
        deactivated: (_) => '✗',
        queued: (_) => '⋯',
        dequeued: (_) => '↺',
      );

      final action = change.map(
        activated: (c) => 'activated${c.previousActive != null ? " (replaced ${c.previousActive!.id})" : ""}',
        deactivated: (_) => 'deactivated',
        queued: (_) => 'queued',
        dequeued: (_) => 'dequeued',
      );

      log('$emoji [${change.surface}] ${change.item.id} $action');
    }

    log('─' * 50);
    log('Summary: ${diff.itemsActivated.length} activated, '
        '${diff.itemsDeactivated.length} deactivated, '
        '${diff.itemsQueued.length} queued, '
        '${diff.itemsDequeued.length} dequeued');
    log('═' * 50);
  }
}
```

Simpler version using convenience getters:

```dart theme={null}
@override
FutureOr<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}');
    }
  }
}
```

## Transition observers vs Surface observers

<AccordionGroup>
  <Accordion title="When to use Transition Observers">
    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
  </Accordion>

  <Accordion title="When to use Surface Observers">
    Use [surface observers](/advanced/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
  </Accordion>
</AccordionGroup>

<Tip>
  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.
</Tip>

## Important notes

<Warning>
  **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.

  ```dart theme={null}
  // ❌ 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
  }
  ```
</Warning>

<Info>
  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.
</Info>

## Next steps

<CardGroup cols={2}>
  <Card title="Event system" icon="bolt" href="/features/events">
    Handle user interaction events
  </Card>

  {" "}

  <Card title="Guards" icon="shield" href="/core-concepts/guards">
    Control what gets shown
  </Card>

  <Card title="State structure" icon="diagram-project" href="/core-concepts/slots-state">
    Understand state and diffs
  </Card>
</CardGroup>
