Popup hosts are widgets that automatically show and dismiss dialogs, fullscreen overlays, or modal presentations based on Presentum surface state. They build on top of surface observers to add popup-specific features:
Duplicate detection - Prevent showing the same popup twice in quick succession
Conflict resolution - Configure what happens when popups overlap
Automatic queuing - Queue popups to show sequentially
Smart dismissal tracking - Only track dismissals for system-closed popups
Use popup hosts for dialogs, fullscreen pages, bottom sheets, and any modal UI
that requires user interaction and navigation integration.
The PresentumPopupSurfaceStateMixin requires PresentumActiveSurfaceItemObserverMixin as a base. If you havenβt used surface observers before, read the Surface observers guide first.
Both mixins are required. You must add
PresentumActiveSurfaceItemObserverMixin before
PresentumPopupSurfaceStateMixin in your mixin list.
Return the appropriate enum value based on how the popup was dismissed:
PopupPresentResult.userDismissed - User took action (e.g., clicked βBuy Nowβ, converted). The mixin will NOT call markDismissed() because you should have already handled it.
PopupPresentResult.systemDismissed - User closed the popup without taking action (e.g., clicked βXβ or βDismissβ). The mixin WILL call markDismissed() automatically.
PopupPresentResult.notPresented - Could not show the popup (e.g., widget not mounted). The mixin will NOT call markDismissed().
The mixin provides a default markDismissed() implementation that delegates
to context.presentum().markDismissed(). Override only if you need custom
dismissal logic.
State: popup surface has item A showing, items B and C activateWith conflictStrategy = queue:ββ active: Campaign A (showing)ββ queue: [Campaign B, Campaign C]User flow:1. Campaign A dialog shows2. User dismisses A3. Campaign B dialog shows automatically (from queue)4. User dismisses B5. Campaign C dialog shows automatically (from queue)
With other strategies:
ignore: Items B and C are ignored while A shows
replace: A is dismissed, B shows immediately (then C replaces B)
The host watches for popup surface changes and shows dialogs automatically.
This pattern initializes the presentum engine, wraps the child with the
provider scope for context access, and adds the popup host for automatic
dialog management.
// β Goodawait presentum.markShown(entry);await showDialog(...);// β Bad - impression not recordedawait showDialog(...);
Check mounted before presenting
Always check if the widget is mounted before showing popups:
Copy
// β Good@overrideFuture<PopupPresentResult> present(Item entry) async { if (!mounted) return PopupPresentResult.notPresented; await markShown(entry); if (!mounted) return PopupPresentResult.notPresented; final result = await showDialog(...); return result == true ? PopupPresentResult.userDismissed : PopupPresentResult.systemDismissed;}
Return correct PopupPresentResult
Map dialog results to the appropriate enum value:
Copy
// User converted - return userDismissedonPressed: () async { await presentum.markConverted(item); if (context.mounted) { Navigator.pop(context, true); // true = user action }}// User just closed - return systemDismissedonPressed: () => Navigator.pop(context, false) // false = system close// Map in present methodfinal result = await showDialog<bool?>(...);return result == true ? PopupPresentResult.userDismissed // Won't call markDismissed : PopupPresentResult.systemDismissed; // Will call markDismissed
This ensures dismissal tracking is only recorded for non-conversions.
Add both mixins in correct order
Always add PresentumActiveSurfaceItemObserverMixin before PresentumPopupSurfaceStateMixin:
Copy
// β Goodclass _MyState extends State<MyWidget> with PresentumActiveSurfaceItemObserverMixin<...>, PresentumPopupSurfaceStateMixin<...> { }// β Bad - wrong order will cause compilation errorsclass _MyState extends State<MyWidget> with PresentumPopupSurfaceStateMixin<...>, PresentumActiveSurfaceItemObserverMixin<...> { }