What are surfaces?
A surface is a named location in your UI where presentations can appear. Think of them as “slots” where content gets inserted.
Examples:
homeTopBanner - Banner at top of home screen
watchlistHeader - Header in watchlist
popup - Modal overlays and dialogs
profileAlert - Alert on profile page
Surfaces answer the question: WHERE should this be shown?
Defining surfaces
Surfaces are typically enums with the PresentumSurface mixin:
enum AppSurface with PresentumSurface {
homeTopBanner,
watchlistHeader,
watchlistFooter,
profileAlert,
popup,
menuTile;
}
That’s it! The mixin provides a key property automatically.
Production example
From a real app handling Black Friday campaigns:
enum CampaignSurface with PresentumSurface {
popup, // Fullscreen dialogs and modals
watchlistHeader, // Banner at top of watchlist
watchlistFooter, // Banner at bottom of watchlist
menuTile; // Tile in navigation menu
}
See source ->
Variants
Surfaces describe where, variants describe how presentations appear:
enum CampaignVariant with PresentumVisualVariant {
fullscreenDialog, // Full-screen modal
dialog, // Standard dialog
banner, // Banner strip
inline; // Inline card
}
Same surface can support multiple variants:
// Both target the same surface but different styles
PresentumOption(
surface: AppSurface.popup,
variant: CampaignVariant.fullscreenDialog,
// ...
)
PresentumOption(
surface: AppSurface.popup,
variant: CampaignVariant.dialog,
// ...
)
Surface naming
Choose descriptive names that indicate location, not content:
enum AppSurface with PresentumSurface {
homeTopBanner, // Location-based
watchlistHeader, // Location-based
settingsNotice, // Location-based
profileAlert, // Location-based
popup, // Location-based
}
enum AppSurface with PresentumSurface {
campaignBanner, // Content-based (too specific)
tipDialog, // Content-based (too specific)
updateAlert, // Content-based (limits reuse)
}
Why location-based? Surfaces should be reusable. homeTopBanner can show
campaigns, tips, or alerts. campaignBanner artificially limits it.
Accessing surfaces
In guards and outlets, surfaces are type-safe:
// In guards
state.setActive(AppSurface.homeTopBanner, item);
// In outlets
PresentumOutlet(
surface: AppSurface.homeTopBanner,
builder: (context, item) => BannerWidget(item),
)
// Query state
final slot = state.slots[AppSurface.homeTopBanner];
Surface organization
Group related surfaces:
enum AppSurface with PresentumSurface {
// Home screen surfaces
homeTopBanner,
homeInlinePromo,
// Watchlist surfaces
watchlistHeader,
watchlistFooter,
watchlistInline,
// Profile surfaces
profileAlert,
profileBanner,
// Global surfaces
popup,
snackbar;
}
Advanced: String conversion
Add helper methods for serialization:
enum CampaignSurface with PresentumSurface {
popup,
watchlistHeader,
watchlistFooter,
menuTile;
static CampaignSurface fromName(
String name, {
CampaignSurface? fallback,
}) {
return switch (name) {
'popup' => CampaignSurface.popup,
'watchlistHeader' => CampaignSurface.watchlistHeader,
'watchlistFooter' => CampaignSurface.watchlistFooter,
'menuTile' => CampaignSurface.menuTile,
_ => fallback ?? (throw ArgumentError.value(name)),
};
}
}
See production implementation ->
Surface independence
Each surface has its own slot. They don’t interfere unless your guards coordinate them:
slots: {
AppSurface.homeTopBanner: Slot(
active: Campaign A,
queue: [],
),
AppSurface.watchlistHeader: Slot(
active: Tip B,
queue: [Alert C],
),
AppSurface.popup: Slot(
active: Update D,
queue: [],
),
}
Coordinating surfaces
Guards can coordinate multiple surfaces:
class SequencingGuard extends CampaignGuard {
@override
FutureOr<PresentumState> call(
storage, history, state, candidates, context,
) async {
// Show popup only after header is dismissed
final headerSlot = state.slots[CampaignSurface.watchlistHeader];
final headerActive = headerSlot?.active != null;
for (final candidate in candidates) {
if (candidate.surface == CampaignSurface.popup && headerActive) {
continue; // Don't show popup while header is active
}
state.setActive(candidate.surface, candidate);
}
return state;
}
}
See production sequencing ->
Next steps