Skip to main content

Overview

Guards are where your presentation business logic lives. This guide shows you how to build guards from simple to production-level complexity.

Basic guard

Start with a simple guard that sets the highest priority item as active:
final class FeatureSchedulingGuard
    extends PresentumGuard<FeatureItem, AppSurface, AppVariant> {
  FeatureSchedulingGuard({
    required this.eligibilityResolver,
    super.refresh,
  });

  final EligibilityResolver<FeatureItem> eligibilityResolver;

  @override
  Future<PresentumState<FeatureItem, AppSurface, AppVariant>> call(
    PresentumStorage<AppSurface, AppVariant> storage,
    List<PresentumHistoryEntry<FeatureItem, AppSurface, AppVariant>> history,
    PresentumState$Mutable<FeatureItem, AppSurface, AppVariant> state,
    List<FeatureItem> candidates,
    Map<String, Object?> context,
  ) async {
    // 1) Filter: if feature is gone, disabled, ineligible, or dismissed,
    // exclude it from UI.
    final filtered = <FeatureItem>[];

    for (final item in candidates) {
      // Check eligibility (time ranges, segments, etc.)
      final isEligible = await eligibilityResolver.isEligible(item, context);
      if (!isEligible) continue;

      // Check if feature is dismissed
      final dismissedAt = await storage.getDismissedAt(
        item.id,
        surface: item.surface,
        variant: item.variant,
      );
      if (dismissedAt != null) continue;

      filtered.add(item);
    }

    // 3) Project candidates -> slots (active + queue)
    // This is what makes Settings rows and UI banners actually appear.
    state.clearAll();

    final bySurface = <AppSurface, List<FeatureItem>>{};
    for (final item in filtered) {
      (bySurface[item.surface] ??= <FeatureItem>[]).add(item);
    }

    for (final entry in bySurface.entries) {
      final surface = entry.key;
      final items = entry.value;

      int stageOf(FeatureItem i) => i.stage ?? 0;

        items.sort((a, b) {
          final stageCmp = stageOf(a).compareTo(stageOf(b));
          if (stageCmp != 0) return stageCmp;
          return b.priority.compareTo(a.priority);
        });

      state.addAll(surface, items);
    }

    return state;
  }
}

Production guard examples

See real implementations:

See all production guards

Complete guard implementations from a live app

See all reusable guards

Generic guards to sync state, remove ineligible candidates and simple eligibility scheduling