Skip to main content

Architecture

Presentum separates presentation logic into five clear layers:
1

Fetch candidates

Your app fetches presentations from Firebase Remote Config, APIs, or local sources.
2

Feed to engine

Convert to items and feed to Presentum using setCandidates or setCandidatesWithDiff.
3

Guards process

Engine runs all guards in sequence to determine eligibility, apply rules, and update state.
4

State committed

New state is committed and observers are notified.
5

Outlets rebuild

Outlets watching affected surfaces rebuild with new active items.
6

User interacts

User dismisses or converts, which records to storage and may trigger guard re-evaluation.

The five layers

Key concepts

Surfaces

Where presentations appear. Named locations in your UI:
enum AppSurface with PresentumSurface {
  homeTopBanner,
  watchlistHeader,
  popup;
}
Learn more ->

Payloads

What you show. Your domain objects:
class CampaignPayload extends PresentumPayload<AppSurface, CampaignVariant> {
  final String id;
  final int priority;
  final Map<String, Object?> metadata;
  final List<PresentumOption> options;
}
Learn more ->

Items

Concrete decisions: “show this payload with this option on this surface”:
final item = CampaignPresentumItem(
  payload: campaign,
  option: campaign.options.first,
);

// Derived properties
item.id        // "campaign-123::banner::homeTopBanner"
item.surface   // AppSurface.homeTopBanner
item.variant   // CampaignVariant.banner
item.priority  // 100
Learn more ->

Slots

Each surface has one slot containing:
  • Active item - Currently displayed (or null)
  • Queue - FIFO list of items waiting
PresentumSlot(
  surface: AppSurface.homeTopBanner,
  active: campaignItem,
  queue: [tipItem, alertItem],
)
Learn more ->

Guards

Business logic that decides what gets shown:
class CampaignGuard extends PresentumGuard {
  @override
  FutureOr<PresentumState> call(
    storage, history, state, candidates, context,
  ) async {
    // Apply your rules
    return state;
  }
}
Learn more ->

Data flow

1

Fetch candidates

Your app fetches presentations from Firebase, APIs, or local sources.
final campaigns = await fetchCampaigns();
2

Feed to engine

Convert to items and feed to Presentum.
final items = campaigns.map((c) => CampaignItem(
  payload: c,
  option: c.options.first,
)).toList();

engine.setCandidatesWithDiff((state) => items);
3

Guards process

Engine runs all guards to determine eligibility.
// In guard
for (final candidate in candidates) {
  if (await isEligible(candidate)) {
    state.setActive(candidate.surface, candidate);
  }
}
4

State committed

New state is committed and observers notified.
5

Outlets rebuild

Outlets watching affected surfaces rebuild.
PresentumOutlet(
  surface: AppSurface.homeTopBanner,
  builder: (context, item) => BannerWidget(item),
)
6

User interacts

User dismisses or converts, triggering new guard evaluation.
await presentum.markDismissed(item);
// -> Guards re-run
// -> State updates
// -> Outlets rebuild

Production example

Here’s how a production app structures everything:
// Surfaces
enum CampaignSurface with PresentumSurface {
  popup,
  watchlistHeader,
  watchlistFooter,
  menuTile;
}

// Variants
enum CampaignVariant with PresentumVisualVariant {
  fullscreenDialog,
  dialog,
  banner,
  inline;
}

// Payload
class CampaignPayload extends PresentumPayload<CampaignSurface, CampaignVariant> {
  factory CampaignPayload.fromJson(Map<String, Object?> json) { /* ... */ }
  // ... fields
}

// Initialize
final presentum = Presentum(
  storage: storage,
  eventHandlers: [
    PresentumStorageEventHandler(storage: storage),
  ],
  guards: [
    AppOpenedCountGuard(/* ... */),
    AppLifecycleChangedGuard(/* ... */),
    SyncStateWithCandidatesGuard(),
    CampaignSchedulingGuard(eligibility),
    RemoveIneligibleCampaignsGuard(eligibility),
  ],
);

// Provider fetches from Firebase Remote Config
final provider = CampaignProvider(
  storage: storage,
  engine: presentum.config.engine,
  eligibility: eligibility,
  remoteConfig: FirebaseRemoteConfig.instance,
);
See full implementation ->

Next steps