Overview
In this guide, you’ll create a campaign banner system that:
Shows promotional banners
Tracks impressions and dismissals
Respects impression limits and cooldowns
Handles user interactions
Time estimate: 5 minutesPrerequisites: Flutter project with Presentum installed (Installation guide )
Step 1: Define surfaces and variants
Create lib/presentation/surfaces.dart:
lib/presentation/surfaces.dart
import 'package:presentum/presentum.dart' ;
/// Where presentations can appear
enum AppSurface with PresentumSurface {
homeTopBanner,
profileAlert,
popup;
}
/// How presentations are displayed
enum CampaignVariant with PresentumVisualVariant {
banner,
dialog,
inline;
}
Surfaces are locations (homeTopBanner, popup). Variants are display
styles (banner, dialog).
Step 2: Create payload and option classes
Create lib/presentation/payload.dart:
lib/presentation/payload.dart
import 'package:presentum/presentum.dart' ;
import 'surfaces.dart' ;
class CampaignPresentumOption
extends PresentumOption < AppSurface , CampaignVariant > {
const CampaignPresentumOption ({
required this .surface,
required this .variant,
required this .isDismissible,
this .stage,
this .maxImpressions,
this .cooldownMinutes,
this .alwaysOnIfEligible = false ,
});
@override
final AppSurface surface;
@override
final CampaignVariant variant;
@override
final bool isDismissible;
@override
final int ? stage;
@override
final int ? maxImpressions;
@override
final int ? cooldownMinutes;
@override
final bool alwaysOnIfEligible;
}
class CampaignPayload
extends PresentumPayload < AppSurface , CampaignVariant > {
const CampaignPayload ({
required this .id,
required this .priority,
required this .metadata,
required this .options,
});
@override
final String id;
@override
final int priority;
@override
final Map < String , Object ?> metadata;
@override
final List < CampaignPresentumOption > options;
}
class CampaignPresentumItem
extends PresentumItem < CampaignPayload , AppSurface , CampaignVariant > {
const CampaignPresentumItem ({
required this .payload,
required this .option,
});
@override
final CampaignPayload payload;
@override
final CampaignPresentumOption option;
}
See production payload with JSON serialization ->
Step 3: Implement storage
Create lib/presentation/storage.dart:
lib/presentation/storage.dart
import 'dart:async' ;
import 'package:presentum/presentum.dart' ;
import 'surfaces.dart' ;
class InMemoryStorage implements PresentumStorage < AppSurface , CampaignVariant > {
final Map < String , DateTime > _lastShown = {};
final Map < String , int > _shownCount = {};
final Map < String , DateTime > _dismissedAt = {};
@override
Future < void > init () async {}
@override
Future < void > clear () async {
_lastShown. clear ();
_shownCount. clear ();
_dismissedAt. clear ();
}
@override
FutureOr < void > recordShown (
String itemId, {
required AppSurface surface,
required CampaignVariant variant,
required DateTime at,
}) {
final key = _makeKey (itemId, surface, variant);
_lastShown[key] = at;
_shownCount[key] = (_shownCount[key] ?? 0 ) + 1 ;
}
@override
FutureOr < DateTime ?> getLastShown (
String itemId, {
required AppSurface surface,
required CampaignVariant variant,
}) {
final key = _makeKey (itemId, surface, variant);
return _lastShown[key];
}
@override
FutureOr < int > getShownCount (
String itemId, {
required Duration period,
required AppSurface surface,
required CampaignVariant variant,
}) {
final key = _makeKey (itemId, surface, variant);
return _shownCount[key] ?? 0 ;
}
@override
FutureOr < void > recordDismissed (
String itemId, {
required AppSurface surface,
required CampaignVariant variant,
required DateTime at,
}) {
final key = _makeKey (itemId, surface, variant);
_dismissedAt[key] = at;
}
@override
FutureOr < DateTime ?> getDismissedAt (
String itemId, {
required AppSurface surface,
required CampaignVariant variant,
}) {
final key = _makeKey (itemId, surface, variant);
return _dismissedAt[key];
}
@override
FutureOr < void > recordConverted (
String itemId, {
required AppSurface surface,
required CampaignVariant variant,
required DateTime at,
}) {
// Implement conversion tracking
}
String _makeKey ( String itemId, Enum surface, Enum variant) =>
' $ itemId :: ${ surface . name } :: ${ variant . name } ' ;
}
This is an in-memory implementation for demo purposes. For production, use
SharedPreferences or a backend.
See production SharedPreferences storage ->
Step 4: Create a guard
Create lib/presentation/guards.dart:
lib/presentation/guards.dart
import 'dart:async' ;
import 'package:presentum/presentum.dart' ;
import 'payload.dart' ;
import 'surfaces.dart' ;
class CampaignGuard
extends PresentumGuard < CampaignPresentumItem , AppSurface , CampaignVariant > {
@override
FutureOr < PresentumState < CampaignPresentumItem , AppSurface , CampaignVariant >> call (
PresentumStorage storage,
List < PresentumHistoryEntry > history,
PresentumState$Mutable state,
List < CampaignPresentumItem > candidates,
Map < String , Object ?> context,
) async {
for ( final candidate in candidates) {
final option = candidate.option;
// Check impression limit
if (option.maxImpressions case final max ? ) {
final count = await storage. getShownCount (
candidate.id,
period : const Duration (days : 365 ),
surface : candidate.surface,
variant : candidate.variant,
);
if (count >= max) continue ;
}
// Check cooldown
if (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 = DateTime . now (). difference (last).inMinutes;
if (minutesSince < cooldown) continue ;
}
}
// All checks passed
state. setActive (candidate.surface, candidate);
}
return state;
}
}
See production guards ->
Step 5: Initialize Presentum
Update lib/main.dart:
import 'package:flutter/material.dart' ;
import 'package:presentum/presentum.dart' ;
import 'presentation/surfaces.dart' ;
import 'presentation/payload.dart' ;
import 'presentation/storage.dart' ;
import 'presentation/guards.dart' ;
late final Presentum < CampaignPresentumItem , AppSurface , CampaignVariant >
campaignPresentum;
void main () async {
WidgetsFlutterBinding . ensureInitialized ();
// Initialize storage
final storage = InMemoryStorage ();
await storage. init ();
// Create Presentum instance
campaignPresentum = Presentum (
storage : storage,
eventHandlers : [
PresentumStorageEventHandler (storage : storage),
],
guards : [
CampaignGuard (),
],
);
runApp ( const MyApp ());
}
class MyApp extends StatelessWidget {
const MyApp ({ super .key});
@override
Widget build ( BuildContext context) {
// Wrap app with Presentum engine
return campaignPresentum.config.engine. build (
context,
MaterialApp (
title : 'Presentum Quickstart' ,
home : const HomeScreen (),
),
);
}
}
See production initialization ->
Step 6: Create an outlet
Create lib/widgets/campaign_outlet.dart:
lib/widgets/campaign_outlet.dart
import 'package:flutter/material.dart' ;
import 'package:presentum/presentum.dart' ;
import '../presentation/surfaces.dart' ;
import '../presentation/payload.dart' ;
class HomeTopBannerOutlet extends StatelessWidget {
const HomeTopBannerOutlet ({ super .key});
@override
Widget build ( BuildContext context) {
return PresentumOutlet < CampaignPresentumItem , AppSurface , CampaignVariant >(
surface : AppSurface .homeTopBanner,
builder : (context, item) {
return Container (
margin : const EdgeInsets . all ( 16 ),
padding : const EdgeInsets . all ( 16 ),
decoration : BoxDecoration (
color : Colors .blue,
borderRadius : BorderRadius . circular ( 12 ),
),
child : Row (
children : [
Expanded (
child : Text (
item.metadata[ 'title' ] as String ? ?? '' ,
style : const TextStyle (
color : Colors .white,
fontSize : 18 ,
fontWeight : FontWeight .bold,
),
),
),
IconButton (
icon : const Icon ( Icons .close, color : Colors .white),
onPressed : () {
context
. presentum < CampaignPresentumItem , AppSurface , CampaignVariant >()
. markDismissed (item);
},
),
],
),
);
},
);
}
}
Step 7: Use the outlet
Add the outlet to your home screen:
lib/screens/home_screen.dart
import 'package:flutter/material.dart' ;
import '../widgets/campaign_outlet.dart' ;
class HomeScreen extends StatelessWidget {
const HomeScreen ({ super .key});
@override
Widget build ( BuildContext context) {
return Scaffold (
appBar : AppBar (title : const Text ( 'Home' )),
body : Column (
children : [
const HomeTopBannerOutlet (),
Expanded (
child : ListView (
children : const [
ListTile (title : Text ( 'Your app content' )),
],
),
),
],
),
floatingActionButton : FloatingActionButton (
onPressed : () => _addTestCampaign (context),
child : const Icon ( Icons .add),
),
);
}
void _addTestCampaign ( BuildContext context) {
final campaign = CampaignPayload (
id : 'test-campaign' ,
priority : 100 ,
metadata : { 'title' : 'Welcome to Presentum!' },
options : [
CampaignPresentumOption (
surface : AppSurface .homeTopBanner,
variant : CampaignVariant .banner,
maxImpressions : 3 ,
cooldownMinutes : 60 ,
isDismissible : true ,
),
],
);
final item = CampaignPresentumItem (
payload : campaign,
option : campaign.options.first,
);
// Feed to engine
context
. presentum < CampaignPresentumItem , AppSurface , CampaignVariant >()
.config
.engine
. setCandidates (
(state, current) => [...current, item],
);
}
}
Step 8: Run your app
Run the app
bash flutter run
Tap the + button
This creates and displays a test campaign banner.
Dismiss the banner
Click the X to dismiss it.
Try again
Notice cooldown and impression limits work automatically!
Congratulations! You’ve built your first Presentum presentation.
Next steps
Common questions
How do I integrate with Firebase Remote Config?
Fetch campaigns from Firebase and feed them using setCandidates or
setCandidatesWithDiff. See Remote Config recipe
for a complete example.
Can I use multiple Presentum instances?
Yes! Create separate instances for different presentation types (campaigns,
tips, app updates). Each has independent state.
Guards are functions - easy to test! Mock storage and verify state
mutations. See Testing guide .