Skip to main content

Overview

The event system lets you react to presentation lifecycle events like shown, dismissed, and converted. Events flow through event handlers that you register with Presentum. Each handler can perform actions like recording to storage, sending analytics, or triggering business logic.

Built-in events

Presentum provides three core events:

PresentumShownEvent

Fired when a presentation is displayed to the user

PresentumDismissedEvent

Fired when a user dismisses a presentation

PresentumConvertedEvent

Fired when a user takes action on a presentation

Event structure

All events extend PresentumEvent and include:
abstract class PresentumEvent<TItem, S, V> {
  abstract final TItem item;        // The presentation item
  abstract final DateTime timestamp; // When the event occurred
}

PresentumShownEvent

final event = PresentumShownEvent(
  item: campaignItem,
  timestamp: DateTime.now(),
);

PresentumDismissedEvent

final event = PresentumDismissedEvent(
  item: campaignItem,
  timestamp: DateTime.now(),
);

PresentumConvertedEvent

final event = PresentumConvertedEvent(
  item: campaignItem,
  timestamp: DateTime.now(),
  conversionMetadata: {'source': 'banner', 'campaign_id': 'black-friday'},
);

Event handlers

Create handlers by implementing IPresentumEventHandler:
class AnalyticsEventHandler
    implements IPresentumEventHandler<CampaignItem, AppSurface, CampaignVariant> {
  AnalyticsEventHandler(this.analytics);

  final AnalyticsService analytics;

  @override
  FutureOr<void> call(PresentumEvent event) {
    switch (event) {
      case PresentumShownEvent(:final item, :final timestamp):
        analytics.logImpression(
          itemId: item.id,
          surface: item.surface.name,
          variant: item.variant.name,
          timestamp: timestamp,
        );

      case PresentumDismissedEvent(:final item, :final timestamp):
        analytics.logDismissal(
          itemId: item.id,
          surface: item.surface.name,
          timestamp: timestamp,
        );

      case PresentumConvertedEvent(:final item, :final timestamp, :final conversionMetadata):
        analytics.logConversion(
          itemId: item.id,
          metadata: {
            ...item.metadata,
            ...?conversionMetadata,
          },
          timestamp: timestamp,
        );
    }
  }
}

Built-in storage handler

PresentumStorageEventHandler automatically records events to storage:
final storageHandler = PresentumStorageEventHandler<
  CampaignItem,
  AppSurface,
  CampaignVariant
>(storage: storage);

// Handles:
// - PresentumShownEvent -> storage.recordShown()
// - PresentumDismissedEvent -> storage.recordDismissed()
// - PresentumConvertedEvent -> storage.recordConverted()

Register handlers

Add handlers when creating Presentum:
presentum = Presentum(
  storage: storage,
  guards: guards,
  eventHandlers: [
    PresentumStorageEventHandler(storage: storage),
    AnalyticsEventHandler(analyticsService),
    LoggingEventHandler(),
    // Add more as needed
  ],
);
Handlers run in order. Each receives the same event.

Trigger events

Events are automatically triggered when you call:
// Triggers PresentumShownEvent
await presentum.markShown(item);

// Triggers PresentumDismissedEvent
await presentum.markDismissed(item);

// Triggers PresentumConvertedEvent
await presentum.markConverted(
  item,
  conversionMetadata: {'source': 'cta_button'},
);

Custom events

Create your own events by extending PresentumEvent:
// 1. Define event
final class CampaignViewedEvent<TItem, S, V> extends PresentumEvent<TItem, S, V> {
  const CampaignViewedEvent({
    required this.item,
    required this.timestamp,
    required this.viewDuration,
  });

  @override
  final TItem item;

  @override
  final DateTime timestamp;

  final Duration viewDuration;
}

// 2. Create handler
class ViewDurationHandler implements IPresentumEventHandler<Item, Surface, Variant> {
  @override
  FutureOr<void> call(PresentumEvent event) {
    if (event is CampaignViewedEvent) {
      analytics.logViewDuration(
        event.item.id,
        event.viewDuration,
      );
    }
  }
}

// 3. Trigger manually
await presentum.addEvent(
  CampaignViewedEvent(
    item: campaignItem,
    timestamp: DateTime.now(),
    viewDuration: const Duration(seconds: 30),
  ),
);

Production example

Here’s a complete event handling setup from a production app:
class CampaignAnalyticsHandler
    implements IPresentumEventHandler<CampaignItem, CampaignSurface, CampaignVariant> {
  CampaignAnalyticsHandler(this.analytics);

  final AnalyticsService analytics;

  @override
  FutureOr<void> call(PresentumEvent event) async {
    switch (event) {
      case PresentumShownEvent(:final item):
        await analytics.logEvent(
          name: 'campaign_shown',
          parameters: {
            'campaign_id': item.payload.id,
            'surface': item.surface.name,
            'variant': item.variant.name,
            'priority': item.priority,
            ...item.metadata,
          },
        );

      case PresentumDismissedEvent(:final item):
        await analytics.logEvent(
          name: 'campaign_dismissed',
          parameters: {
            'campaign_id': item.payload.id,
            'surface': item.surface.name,
            'variant': item.variant.name,
          },
        );

      case PresentumConvertedEvent(:final item, :final conversionMetadata):
        await analytics.logEvent(
          name: 'campaign_converted',
          parameters: {
            'campaign_id': item.payload.id,
            'surface': item.surface.name,
            'variant': item.variant.name,
            ...?conversionMetadata,
          },
        );
    }
  }
}

// Register
presentum = Presentum(
  storage: storage,
  eventHandlers: [
    PresentumStorageEventHandler(storage: storage),
    CampaignAnalyticsHandler(analyticsService),
  ],
  guards: guards,
);

Event handler patterns

Logging handler

class LoggingEventHandler implements IPresentumEventHandler<Item, Surface, Variant> {
  @override
  FutureOr<void> call(PresentumEvent event) {
    log('[Presentum Event] ${event.runtimeType}: ${event.item.id}');
  }
}

Backend sync handler

class BackendSyncHandler implements IPresentumEventHandler<Item, Surface, Variant> {
  BackendSyncHandler(this.api);

  final ApiClient api;

  @override
  FutureOr<void> call(PresentumEvent event) async {
    await api.post('/events', {
      'type': event.runtimeType.toString(),
      'item_id': event.item.id,
      'timestamp': event.timestamp.toIso8601String(),
    });
  }
}

BLoC integration handler

class BlocEventHandler implements IPresentumEventHandler<Item, Surface, Variant> {
  BlocEventHandler(this.bloc);

  final CampaignBloc bloc;

  @override
  FutureOr<void> call(PresentumEvent event) {
    switch (event) {
      case PresentumShownEvent(:final item):
        bloc.add(CampaignShown(item.id));
      case PresentumDismissedEvent(:final item):
        bloc.add(CampaignDismissed(item.id));
      case PresentumConvertedEvent(:final item):
        bloc.add(CampaignConverted(item.id));
    }
  }
}

Best practices

Event handlers run synchronously. Avoid slow operations.Bad:
@override
Future<void> call(PresentumEvent event) async {
  await heavyDatabaseOperation(); // Blocks everything
}
Good:
@override
Future<void> call(PresentumEvent event) {
  unawaited(heavyDatabaseOperation()); // Fire and forget
}
Always include the storage handler to record events:
eventHandlers: [
  PresentumStorageEventHandler(storage: storage), // ✅ Always first
  AnalyticsEventHandler(analytics),
  // Other handlers...
],

Next steps