Skip to main content

State structure

Presentum manages state as a map of slots, where each slot represents one surface in your app.

Visual representation

Imagine three surfaces with different presentations:
homeTopBanner
β”œβ”€ active: Campaign "Black Friday" (priority: 100)
└─ queue: [
     Campaign "New Year" (priority: 80),
     Tip "Swipe to refresh" (priority: 50)
   ]

profileAlert
β”œβ”€ active: AppUpdate "Version 2.0" (priority: 200)
└─ queue: []

popup
β”œβ”€ active: null
└─ queue: [
     Tip "Enable notifications" (priority: 60)
   ]

State object

Here’s the actual state object:
final state = PresentumState$Immutable(
  intention: PresentumStateIntention.auto,
  slots: {
    AppSurface.homeTopBanner: PresentumSlot(
      surface: AppSurface.homeTopBanner,
      active: CampaignItem(
        payload: CampaignPayload(
          id: 'black-friday-2025',
          priority: 100,
          metadata: {
            'title': 'Black Friday Sale',
            'discount': '50%',
          },
          options: [/* ... */],
        ),
        option: CampaignOption(/* ... */),
      ),
      queue: [
        CampaignItem(/* New Year */),
        TipItem(/* Swipe to refresh */),
      ],
    ),
    
    AppSurface.profileAlert: PresentumSlot(
      surface: AppSurface.profileAlert,
      active: AppUpdateItem(/* ... */),
      queue: [],
    ),
    
    AppSurface.popup: PresentumSlot(
      surface: AppSurface.popup,
      active: null,
      queue: [TipItem(/* Enable notifications */)],
    ),
  },
);

Slots

Each slot is a container for one surface:
PresentumSlot<TItem, S, V> {
  final S surface;           // Which surface
  final TItem? active;       // Currently showing (or null)
  final List<TItem> queue;   // Waiting items (FIFO)
}

Active item

The active item is what’s currently displayed. Only one item can be active per surface.
final slot = state.slots[AppSurface.homeTopBanner];
final activeItem = slot?.active;

if (activeItem != null) {
  print('Showing: ${activeItem.id}');
}

Queue

The queue is a FIFO (First In, First Out) list of items waiting their turn. When the active item is dismissed, the first queued item automatically becomes active: Before dismiss:
homeTopBanner
β”œβ”€ active: "Black Friday"
└─ queue: ["New Year", "Swipe tip"]
After dismiss:
homeTopBanner
β”œβ”€ active: "New Year"  ← Promoted
└─ queue: ["Swipe tip"]
This happens automatically via state.clearActive(surface) or presentum.markDismissed(item).

State intentions

Every state change has an intention controlling history management:
enum PresentumStateIntention {
  auto,     // Default: update history when values change
  replace,  // Overwrite last history entry
  append,   // Force new history entry
  cancel,   // Abort transition
}

Usage in guards

@override
FutureOr<PresentumState> call(
  storage, history, state, candidates, context,
) async {
  // Cancel if user offline
  if (!isOnline) {
    state.intention = PresentumStateIntention.cancel;
    return state;
  }

  // Replace last history entry
  state.intention = PresentumStateIntention.replace;
  state.setActive(surface, item);
  
  return state;
}

State queries

Access and query state:
final state = presentum.state;

// All active items across surfaces
final allActive = state.activeItems;

// Active item for specific surface
final bannerItem = state.slots[AppSurface.homeTopBanner]?.active;

// All surfaces with active items
final activeSurfaces = state.activeSurfaces;

Mutable vs Immutable

Presentum uses two state types:
Read-only snapshot exposed to widgets and observers.
// In widgets
final state = presentum.state; // Immutable
final item = state.slots[surface]?.active;

// state.slots[surface] = newSlot; // ❌ Won't compile
Benefits:
  • Predictable - never changes unexpectedly
  • Testable - compare with ==
  • Debuggable - inspect snapshots

State mutations

Inside guards, use these methods:
// Set active item
state.setActive(surface, item);

// With intention
state.setActive(
  surface,
  item,
  intention: PresentumStateIntention.replace,
);

// Clear active (promotes queue)
state.clearActive(surface);

Serialization

State can be serialized for persistence or debugging:
final json = state.toJson(
  encodeItem: (item) => {
    'id': item.id,
    'priority': item.priority,
    'surface': item.surface.name,
    'variant': item.variant.name,
    'metadata': item.metadata,
  },
);
Result:
{
  "intention": "auto",
  "slots": [
    {
      "surface": "homeTopBanner",
      "active": {
        "id": "black-friday-2025",
        "priority": 100,
        "metadata": {"title": "Black Friday Sale"}
      },
      "queue": [
        {"id": "new-year-promo", "priority": 80},
        {"id": "tip-swipe", "priority": 50}
      ]
    }
  ]
}

History

Presentum tracks all state changes:
final history = presentum.observer.history;

// Each entry contains
for (final entry in history) {
  print('At ${entry.timestamp}:');
  print('  Active surfaces: ${entry.state.activeSurfaces}');
}
Enable time-travel debugging, analytics, or restoration after login.

Next steps