Guards are the brain of Presentum. They contain your business logic for deciding what gets shown, when, and in what order.Guards run every time candidates change or when external triggers fire. They inspect:
Current state and history
Storage (impressions, dismissals, conversions)
Candidates (potential presentations)
Context (user data, app state)
Then they mutate state by adding, removing, or reordering items in slots.
Guards are your primary tool for scheduling presentations, removing ineligible
items, periodic refreshes, and complex eligibility rules.
This guard from a production app handles priority, sequencing, impression limits, and cooldowns:
Copy
final class CampaignSchedulingGuard extends CampaignGuard { CampaignSchedulingGuard({required this.eligibility}); final EligibilityResolver<HasMetadata> eligibility; @override FutureOr<CampaignPresentumState> call( PresentumStorage storage, List<CampaignPresentumHistoryEntry> history, CampaignPresentumState$Mutable state, List<CampaignPresentumItem> candidates, Map<String, Object?> context, ) async { // 1) eligibility filter per campaign id. final eligibleEntries = <CampaignPresentumItem>[]; for (final entry in candidates) { final isEligible = await eligibility.isEligible(entry.payload, context); if (isEligible) eligibleEntries.add(entry); } if (eligibleEntries.isEmpty) return state; // Highest priority first (per campaign), then by stage. eligibleEntries.sort((a, b) { final p = b.priority.compareTo(a.priority); if (p != 0) return p; final sa = a.stage ?? 0; final sb = b.stage ?? 0; return sa.compareTo(sb); }); // Compute header-dismissed flag per campaign for sequencing logic. var hasHeader = true; var headerDismissed = false; if (eligibleEntries.any( (entry) => entry.surface == CampaignSurface.homeTopBanner, )) { for (final entry in eligibleEntries) { if (entry.surface != CampaignSurface.homeTopBanner) continue; final dismissedAt = await storage.getDismissedAt( entry.id, surface: entry.surface, variant: entry.variant, ); if (dismissedAt != null && entry.option.isDismissible) { headerDismissed = true; break; } final cooldownMinutes = entry.option.cooldownMinutes; final until = dismissedAt != null && cooldownMinutes != null ? dismissedAt.add(Duration(minutes: cooldownMinutes)) : null; headerDismissed = until != null && until.isAfter(DateTime.now()); } } else { hasHeader = false; } // 2) always-on inline/banner per surface with header/footer sequencing. for (final entry in eligibleEntries) { final p = entry.option; if (!p.alwaysOnIfEligible) continue; final isHeaderDismissed = headerDismissed; // Header: show only until dismissed. if (entry.surface == CampaignSurface.homeTopBanner && hasHeader && isHeaderDismissed) { continue; } // Footer: enable only after header has been dismissed. if (entry.surface == CampaignSurface.homeFooterBanner && !isHeaderDismissed && hasHeader) { continue; } // Do not override higher-priority already set item. final slot = state.slots[entry.surface]; if (slot?.active != null) continue; state.setActive(entry.surface, entry); } // If we had an active watchlist header in the history, don't show a popup. final hadAnActiveHomeTopBanner = history.isNotEmpty && history.any( (entry) => entry.state.slots.values.any( (value) => value.active?.option.surface == CampaignSurface.homeTopBanner, ), ); if (hadAnActiveHomeTopBanner) return state; // 3) popup scheduling: determine order and set active + queue. final popupCandidates = await _popupCandidates( storage, eligibleEntries, state, context, headerDismissed || !hasHeader, ); if (popupCandidates.isEmpty) return state; // First candidate becomes active, rest form the queue. final active = popupCandidates.first; state.setActive(CampaignSurface.popup, active); if (popupCandidates.length > 1) { state.setQueue(CampaignSurface.popup, popupCandidates.sublist(1)); } return state; } /// Returns popup-eligible entries in display order. Future<List<CampaignPresentumItem>> _popupCandidates( PresentumStorage storage, List<CampaignPresentumItem> items, PresentumState$Mutable< CampaignPresentumItem, CampaignSurface, CampaignVariant > state, Map<String, Object?> context, bool headerDismissed, ) async { ... }}
Removes items that are no longer eligible (e.g., expired campaigns). This guard is generic and can be reused in any presentum:
Copy
abstract base class IRemoveIneligibleCandidatesGuard< TItem extends PresentumItem<PresentumPayload<S, V>, S, V>, S extends PresentumSurface, V extends PresentumVisualVariant> extends PresentumGuard<TItem, S, V> { /// {@macro remove_ineligible_candidates_guard} IRemoveIneligibleCandidatesGuard({required this.eligibility, super.refresh}); final EligibilityResolver<HasMetadata> eligibility; @override FutureOr<PresentumState<TItem, S, V>> call( PresentumStorage storage, List<PresentumHistoryEntry<TItem, S, V>> history, PresentumState$Mutable<TItem, S, V> state, List<TItem> candidates, Map<String, Object?> context, ) async { final checkedItems = <String>{}; final ineligibleItems = <String>{}; // First pass: identify all ineligible items from the current state and // current candidates. final slots = [...state.slots.entries]; for (final entry in slots) { final slot = entry.value; if (slot.active case final active?) { final itemId = active.id; // Only check eligibility once per distinct campaign if (checkedItems.contains(itemId)) continue; checkedItems.add(itemId); final eligible = await eligibility.isEligible(active, <String, Object?>{ ...context, }); if (!eligible) { ineligibleItems.add(itemId); } } // Also check items in queue for (final queuedItem in slot.queue) { final itemId = queuedItem.id; if (checkedItems.contains(itemId)) continue; checkedItems.add(itemId); final eligible = await eligibility.isEligible( queuedItem, <String, Object?>{...context}, ); if (!eligible) { ineligibleItems.add(itemId); } } } // Second pass: remove all ineligible items from all surfaces in the // candidate state. if (ineligibleItems.isNotEmpty) { final slots = [...state.slots.entries]; for (final entry in slots) { final surface = entry.key; final slot = entry.value; final active = slot.active; final queue = slot.queue; final activeIneligible = active?.payload.id != null && ineligibleItems.contains(active!.id); // Filter queue to remove ineligible items. final filteredQueue = queue .where((queuedItem) => !ineligibleItems.contains(queuedItem.id)) .toList(); // If nothing was ineligible for this surface, skip. final hadIneligibleInQueue = filteredQueue.length != queue.length; if (!activeIneligible && !hadIneligibleInQueue) continue; // Rebuild slot in `next` with only eligible items. if (activeIneligible) { if (filteredQueue.isNotEmpty) { // Promote first eligible item to active. final nextActive = filteredQueue.first; final nextQueue = filteredQueue.length > 1 ? filteredQueue.sublist(1) : <TItem>[]; state.setActive(surface, nextActive); if (nextQueue.isNotEmpty) { state.setQueue(surface, nextQueue); } } else { // No eligible items remain on this surface. Touch the surface in // state so that followβup guards do not restore stale items. state.clearSurface(surface); } } else { // Active is still eligible, only the queue changed. if (active != null) { state.setActive(surface, active); } state.setQueue(surface, filteredQueue); } } } return state; }}
Keeps state synchronized with latest candidate data using diff algorithm. This
guard is generic and can be reused in any presentum
Copy
abstract base class ISyncStateWithCandidatesGuard< TItem extends PresentumItem<PresentumPayload<S, V>, S, V>, S extends PresentumSurface, V extends PresentumVisualVariant> extends PresentumGuard<TItem, S, V> { /// {@macro sync_state_with_candidates_guard} ISyncStateWithCandidatesGuard({super.refresh}); @override FutureOr<PresentumState<TItem, S, V>> call( PresentumStorage storage, List<PresentumHistoryEntry<TItem, S, V>> history, PresentumState$Mutable<TItem, S, V> state, List<TItem> candidates, Map<String, Object?> context, ) { // Build a map of candidates by their unique key (id + surface + variant) // for fast lookup. final candidateMap = <String, TItem>{}; for (final candidate in candidates) { final key = candidate.id; candidateMap[key] = candidate; } // Process each surface's slot. final slots = [...state.slots.entries]; for (final surfaceEntry in slots) { final surface = surfaceEntry.key; final slot = surfaceEntry.value; // Collect current items in this slot (active + queue). final currentItems = <TItem>[?slot.active, ...slot.queue]; if (currentItems.isEmpty) continue; // Build list of items that should remain, checking against candidates. final syncedItems = <TItem>[]; var itemsChanged = false; for (final currentItem in currentItems) { final key = currentItem.id; final candidateMatch = candidateMap[key]; if (candidateMatch == null) { // Item no longer in candidates - mark for removal. itemsChanged = true; continue; } // Check if content has changed using DiffUtil's content comparison. final contentsChanged = !areContentsTheSame( currentItem, candidateMatch, ); if (contentsChanged) { // Replace with updated version from candidates. syncedItems.add(candidateMatch); itemsChanged = true; } else { // Item unchanged, keep as-is. syncedItems.add(currentItem); } } // If anything changed, update the slot. if (itemsChanged) { if (syncedItems.isEmpty) { // All items removed - clear the surface. state.clearSurface(surface); } else { // Update active and queue. final newActive = syncedItems.first; final newQueue = syncedItems.length > 1 ? syncedItems.sublist(1) : <TItem>[]; state.setActive(surface, newActive); if (newQueue.isNotEmpty) { state.setQueue(surface, newQueue); } else if (slot.queue.isNotEmpty) { // Clear queue if it was previously non-empty. state.setQueue(surface, <TItem>[]); } } } } return state; } bool areContentsTheSame(TItem oldItem, TItem newItem) { ... }
// Set active item for a surfacestate.setActive(surface, item);// With intention controlstate.setActive( surface, item, intention: PresentumStateIntention.replace,);
Copy
// Add to queuestate.enqueue(surface, item);// Set entire queuestate.setQueue(surface, [item1, item2, item3]);// Remove from queuestate.dequeue(surface);
Copy
// Remove by predicatestate.removeWhere((item) => item.priority < 50);// Remove from specific surfacestate.removeFromSurface( surface, (item) => item.id == 'expired-campaign',);// Remove by IDstate.removeById('campaign-123');// Clear entire surfacestate.clearSurface(surface);
class CooldownGuard extends MyGuard { @override FutureOr<PresentumState> call( storage, history, state, candidates, context, ) async { final now = DateTime.now(); for (final candidate in candidates) { if (candidate.option.cooldownMinutes case final cooldown?) { final lastShown = await storage.getLastShown( candidate.id, surface: candidate.surface, variant: candidate.variant, ); if (lastShown case final last?) { final minutesSince = now.difference(last).inMinutes; if (minutesSince < cooldown) continue; // Still in cooldown } } state.setActive(candidate.surface, candidate); } return state; }}
class SequencingGuard extends MyGuard { @override FutureOr<PresentumState> call( storage, history, state, candidates, context, ) async { // Group by surface final bySurface = <Surface, List<Item>>{}; for (final candidate in candidates) { bySurface.putIfAbsent(candidate.surface, () => []).add(candidate); } // For each surface, set highest priority as active, rest in queue for (final entry in bySurface.entries) { final surface = entry.key; final items = entry.value; // Sort by priority items.sort((a, b) => b.priority.compareTo(a.priority)); state.setActive(surface, items.first); if (items.length > 1) { state.setQueue(surface, items.sublist(1)); } } return state; }}
Each guard should have one responsibility. Donβt create one giant guard that
does everything. Good:ImpressionLimitGuard, CooldownGuard,
SegmentTargetingGuardBad:MegaGuardThatDoesEverything
Use eligibility system
For complex conditions, use the built-in eligibility system instead of
manual if/else chains.
Copy
final eligible = await eligibilityResolver.isEligible( candidate.payload, context,);