What is storage?
Storage is the persistence layer for tracking presentation events. It records:
- Impressions - When presentations are shown
- Dismissals - When users close presentations
- Conversions - When users take action
Guards and the engine use storage to make decisions about what to show.
Storage interface
Implement PresentumStorage<S, V>:
abstract interface class PresentumStorage<
S extends PresentumSurface,
V extends PresentumVisualVariant
> {
Future<void> init();
Future<void> clear();
// Shown tracking
FutureOr<void> recordShown(
String itemId, {
required S surface,
required V variant,
required DateTime at,
});
FutureOr<DateTime?> getLastShown(
String itemId, {
required S surface,
required V variant,
});
FutureOr<int> getShownCount(
String itemId, {
required Duration period,
required S surface,
required V variant,
});
// Dismissal tracking
FutureOr<void> recordDismissed(
String itemId, {
required S surface,
required V variant,
required DateTime at,
});
FutureOr<DateTime?> getDismissedAt(
String itemId, {
required S surface,
required V variant,
});
// Conversion tracking
FutureOr<void> recordConverted(
String itemId, {
required S surface,
required V variant,
required DateTime at,
});
}
SharedPreferences implementation
Production implementation using SharedPreferences:
class CampaignStorage
implements PresentumStorage<CampaignSurface, CampaignVariant> {
late SharedPreferencesWithCache _prefs;
@override
Future<void> init() async {
_prefs = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
}
@override
Future<void> clear() => _prefs.clear();
@override
Future<void> recordShown(
String itemId, {
required CampaignSurface surface,
required CampaignVariant variant,
required DateTime at,
}) async {
final key = _makeKey(itemId, surface, variant);
// Update count
final countKey = '${key}_count';
final currentCount = _prefs.getInt(countKey) ?? 0;
await _prefs.setInt(countKey, currentCount + 1);
// Update last shown
final lastShownKey = '${key}_last_shown';
await _prefs.setString(lastShownKey, at.toIso8601String());
// Append to timestamps list
final timestampsKey = '${key}_timestamps';
final timestamps = _prefs.getStringList(timestampsKey) ?? [];
await _prefs.setStringList(timestampsKey, [
...timestamps,
at.toIso8601String(),
]);
}
@override
Future<DateTime?> getLastShown(
String itemId, {
required CampaignSurface surface,
required CampaignVariant variant,
}) async {
final key = _makeKey(itemId, surface, variant);
final timestampStr = _prefs.getString('${key}_last_shown');
return timestampStr != null ? DateTime.parse(timestampStr) : null;
}
@override
Future<int> getShownCount(
String itemId, {
required Duration period,
required CampaignSurface surface,
required CampaignVariant variant,
}) async {
final key = _makeKey(itemId, surface, variant);
final timestamps = _prefs.getStringList('${key}_timestamps') ?? [];
final cutoff = DateTime.now().subtract(period);
final recent = timestamps
.map(DateTime.parse)
.where((t) => t.isAfter(cutoff))
.length;
return recent;
}
@override
Future<void> recordDismissed(
String itemId, {
required CampaignSurface surface,
required CampaignVariant variant,
required DateTime at,
}) async {
final key = _makeKey(itemId, surface, variant);
await _prefs.setString('${key}_dismissed_at', at.toIso8601String());
}
@override
Future<DateTime?> getDismissedAt(
String itemId, {
required CampaignSurface surface,
required CampaignVariant variant,
}) async {
final key = _makeKey(itemId, surface, variant);
final timestampStr = _prefs.getString('${key}_dismissed_at');
return timestampStr != null ? DateTime.parse(timestampStr) : null;
}
@override
Future<void> recordConverted(
String itemId, {
required CampaignSurface surface,
required CampaignVariant variant,
required DateTime at,
}) async {
final key = _makeKey(itemId, surface, variant);
await _prefs.setString('${key}_converted_at', at.toIso8601String());
}
String _makeKey(String itemId, Enum surface, Enum variant) =>
'${itemId}::${surface.name}::${variant.name}';
}
See full production storage ->
Storage key structure
Use compound keys for granular tracking:
Format: {itemId}::{surface}::{variant}_{field}
Examples:
- black_friday_2025::popup::fullscreenDialog_count
- black_friday_2025::popup::fullscreenDialog_last_shown
- black_friday_2025::popup::fullscreenDialog_timestamps
- black_friday_2025::popup::dialog_dismissed_at
- black_friday_2025::watchlistHeader::banner_converted_at
Per-surface-variant tracking lets the same campaign appear differently on
different surfaces with independent impression counts.
In-memory implementation
For testing or session-only tracking:
class InMemoryStorage implements PresentumStorage<AppSurface, CampaignVariant> {
final Map<String, DateTime> _lastShown = {};
final Map<String, int> _shownCount = {};
final Map<String, DateTime> _dismissedAt = {};
final Map<String, DateTime> _convertedAt = {};
@override
Future<void> init() async {}
@override
Future<void> clear() async {
_lastShown.clear();
_shownCount.clear();
_dismissedAt.clear();
_convertedAt.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;
}
// Implement other methods...
String _makeKey(String itemId, Enum surface, Enum variant) =>
'$itemId::${surface.name}::${variant.name}';
}
Backend API implementation
Sync events to a backend:
class ApiStorage implements PresentumStorage<AppSurface, CampaignVariant> {
ApiStorage(this.apiClient);
final ApiClient apiClient;
@override
Future<void> recordShown(
String itemId, {
required AppSurface surface,
required CampaignVariant variant,
required DateTime at,
}) async {
await apiClient.post('/impressions', {
'item_id': itemId,
'surface': surface.name,
'variant': variant.name,
'timestamp': at.toIso8601String(),
});
}
@override
Future<DateTime?> getLastShown(
String itemId, {
required AppSurface surface,
required CampaignVariant variant,
}) async {
final response = await apiClient.get(
'/impressions/last',
params: {
'item_id': itemId,
'surface': surface.name,
'variant': variant.name,
},
);
final timestamp = response['timestamp'] as String?;
return timestamp != null ? DateTime.parse(timestamp) : null;
}
// Implement other methods...
}
Storage event handler
Use PresentumStorageEventHandler to automatically record events:
presentum = Presentum(
storage: storage,
eventHandlers: [
PresentumStorageEventHandler(storage: storage),
// This handler calls storage.recordShown/Dismissed/Converted automatically
],
guards: guards,
);
Learn more about events ->
Best practices
Include item ID, surface, and variant in keys for granular tracking:String _makeKey(String itemId, Enum surface, Enum variant) =>
'$itemId::${surface.name}::${variant.name}';
Handle period-based counts
getShownCount receives a period parameter. Only count impressions within that timeframe:final cutoff = DateTime.now().subtract(period);
final recentCount = timestamps.where((t) => t.isAfter(cutoff)).length;
Initialize storage before Presentum
final storage = MyStorage();
await storage.init(); // ✅ Initialize first
final presentum = Presentum(storage: storage);
Next steps