novvy_ads 1.2.1
novvy_ads: ^1.2.1 copied to clipboard
Flutter plugin for Novvy Ads SDK.
Novvy Ads Flutter Plugin #
Plugin version
1.2.1. This plugin is a thin wrapper over the native Novvy Ads SDK (Android / iOS): it only exposes the SDK top-level entry (initialization, user / content context, email hashing). Ads themselves are created and driven through their own classes.
Features #
- Rewarded Ads — Full-screen video ads that grant a reward on completion (
NovvyRewardedAd) - Interstitial Ads — Full-screen ads shown at natural transition points (
NovvyInterstitialAd) - Mid-roll Ads — Full-screen ads shown during episode playback (
NovvyMidAd) - End-roll Ads — Full-screen ads shown at the end of an episode (
NovvyEndAd) - Scroll Ads — Inline feed ads with visibility-driven playback (
NovvyScrollAd+NovvyScrollAdView) - Pause Ads — Realtime 260×90 dp inline pause cards driven by a player adapter (
NovvyPauseAd+NovvyPauseAdView) - Banner Ads — Persistent inline slots that rotate creatives on a timer, in five IAB tiers, with click callbacks (
NovvyBannerAd+NovvyBannerAdView) - Per-instance callbacks — Each ad instance carries its own
onAdLoaded/onAdShowed/onAdDismissed/onAdFailedToLoadclosures; no central callback registration
Platform Support #
| Platform | Minimum version | Notes |
|---|---|---|
| Android | 7.0 (API 24+) | |
| iOS | 13.0+ | Simulator requires Apple Silicon Mac — see iOS Notes |
Requirements #
| Minimum | |
|---|---|
| Dart | 3.0.0+ |
| Flutter | 3.10.0+ |
| Android | API 24 (Android 7.0) |
| iOS | 13.0 (Xcode 14+, Swift 5.0+) |
Installation #
1. Add dependency #
dependencies:
novvy_ads: ^1.2.1
flutter pub get
Import:
import 'package:novvy_ads/novvy_ads.dart';
appId/apiKey/endpointare provided by the Novvy platform.
2. Configure Android #
Verify minSdk
// android/app/build.gradle
android {
defaultConfig {
minSdk 24
}
}
3. Configure iOS #
Verify deployment target
# ios/Podfile
platform :ios, '13.0'
Install pods
cd ios && pod install
The first pod install downloads the NovvyAds xcframework (~5 MB) automatically; subsequent runs skip the download unless the SDK version changes.
Add required keys to Info.plist
Open ios/Runner/Info.plist and add the following key inside the root <dict>:
App Tracking Transparency usage description (required for iOS 14+):
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
iOS aborts the process at launch if any ATT-backed API is called without
NSUserTrackingUsageDescription. Customize the string to match your app's tone.
Configure Podfile
NovvyAds.xcframework only ships arm64 simulator slices. Exclude x86_64 from simulator builds:
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'x86_64'
end
end
installer.aggregate_targets.each do |aggregate|
aggregate.user_project.native_targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'x86_64'
end
end
aggregate.user_project.save
end
end
Initialize the SDK #
Initialize once as early as possible in app startup (e.g. main() or the root widget's initState). Initialization must complete before any ad is created.
Initialization is callback-driven (mirroring the native SDK): onComplete(success) fires on the main isolate once initialization is truly complete (after UA / IDFA pre-warm). Passing userContext at construction is equivalent to calling setUserContext immediately after.
main() / App startup
↓
NovvyAds.initialize(appId, endpoint, apiKey, userContext?, onComplete)
↓
onComplete(success) // fires after UA/IDFA pre-warm
↓
NovvyAds.setContentContext(...) // drama scenarios: set before each load/attach
NovvyAds.initialize(
appId: 'your_app_id',
endpoint: 'https://bid.novvy.ai',
apiKey: 'your_api_key',
userContext: NovvyUserContext( // optional — can also be set later
userId: 'user_123',
hashedEmail: NovvyAds.helperHashEmail('user@example.com'),
isPaidUser: true,
age: 25,
gender: 'female', // "male" / "female" / "unknown"
dramaWatchHistory: [
NovvyDramaRecord(seriesId: 'S1', lastWatchedEpisode: 5, unlockedEpisode: 8, dramaTitle: 'Series 1'),
NovvyDramaRecord(seriesId: 'S2', lastWatchedEpisode: 3, unlockedEpisode: 3, dramaTitle: 'Series 2'),
],
maximumAdsInOneDrama: 10,
),
onComplete: (success) { // optional
if (!success) log('Novvy init failed');
},
);
NovvyAds.initialize parameters (flat, mirroring the native SDK):
| Parameter | Type | Description |
|---|---|---|
appId |
String |
Required. Application identifier |
endpoint |
String |
Required. Ad service endpoint |
apiKey |
String |
Required. API key sent with each request for authentication |
userContext |
NovvyUserContext? |
Optional. User-targeting context applied at init time (maps to native initialize(userContext:)) |
onComplete |
void Function(bool success)? |
Optional. Fires when initialization completes; false = init failed or platform call errored. Delivered on main isolate |
initialize()can be called again (e.g. after login / logout) to refresh configuration. The native SDK handles repeat initialization idempotently.
NovvyAds Top-Level Entry #
NovvyAds is a static-only entry class handling initialization and context.
| Method | Description |
|---|---|
NovvyAds.initialize({required appId, required endpoint, required apiKey, userContext?, onComplete?}) |
Initialize the SDK. Must be called before creating any ad |
NovvyAds.helperHashEmail(String email) → String |
SHA-256 hash an email (internally trims + lowercases); use for NovvyUserContext.hashedEmail |
NovvyAds.setUserContext(NovvyUserContext context) |
Replace the entire user context (null fields clear existing values) |
NovvyAds.updateUserContext(NovvyUserContext partial) |
Merge user context fields (null fields preserve existing values) |
NovvyAds.setContentContext(NovvyContentContext context) |
Replace the entire content context (null fields clear existing values) |
NovvyAds.updateContentContext(NovvyContentContext partial) |
Merge content context fields (null fields preserve existing values) |
See User & Content Context below for the field details.
Ad Types #
All ad classes extend NovvyAd, and instances are held by the host: keep a reference until the ad finishes (onAdDismissed / onAdHidden), then call destroy().
Except for the pause card and the banner slot, every ad is two-stage — load() first, then show() / attach() once ready (onAdLoaded). Those two have no load(): mounting the widget is the request.
| Section | Class | Format | Show method | Lifecycle callbacks |
|---|---|---|---|---|
| Rewarded | NovvyRewardedAd |
Full-screen | show() |
onAdShowed → onUserEarnedReward → onAdDismissed |
| Interstitial | NovvyInterstitialAd |
Full-screen | show() |
onAdShowed → onAdDismissed |
| Mid-roll | NovvyMidAd |
Full-screen (mid-roll) | show() |
onAdShowed → onAdDismissed |
| End-roll | NovvyEndAd |
Full-screen (post-roll) | show() |
onAdShowed → onAdDismissed |
| Scroll | NovvyScrollAd |
Inline (feed) | NovvyScrollAdView widget |
onAdShowed / onAdHidden / onAdPlaybackTick |
| Pause | NovvyPauseAd |
Inline (pause card, 260×90) | NovvyPauseAdView widget |
onAdShowed / onAdHidden (realtime — no preload) |
| Banner | NovvyBannerAd |
Inline (persistent slot, 5 tiers) | NovvyBannerAdView widget |
onAdLoaded / onAdShowed / onAdClicked / onAdHidden (once per rotation) |
⚠️ Drama scenarios: content context is required. The drama backend needs to know which series and which episode is playing, otherwise it returns no fill. Call
NovvyAds.setContentContext(...)before everyload()/attach()(see User & Content Context).
NovvyAd base class #
| Member | Type | Description |
|---|---|---|
adUnitId |
String |
Ad unit identifier (passed at construction) |
adObjectId |
String |
Internally-generated unique instance ID |
isReady |
bool |
Whether the ad has finished loading |
onAdLoaded |
void Function()? |
Load-success callback |
onAdFailedToLoad |
void Function(String error)? |
Load / request failure callback; error is a description string |
load() |
Future<void> |
Pre-load. Callback-driven: results arrive via onAdLoaded / onAdFailedToLoad, not the returned Future (safe to fire-and-forget). Not used by the pause card |
destroy() |
Future<void> |
Release resources; idempotent |
Error handling: unlike the Android SDK's sealed
NovvyErrorclass, the Flutter surface passes a plainStringerror description toonAdFailedToLoad(no fill also comes through this callback). Possible values are listed under Error Types.
Rewarded Ads — NovvyRewardedAd #
One-shot full-screen ad: load() → show(). When the user finishes watching (the ad becomes closable), onUserEarnedReward fires and the host grants its own reward.
Flow
NovvyAds.setContentContext(...) // drama scenarios only
↓
NovvyRewardedAd(adUnitId)
↓
load() → onAdLoaded // ready (or onAdFailedToLoad)
↓
show() → onAdShowed
↓
onUserEarnedReward // grant reward
↓
onAdDismissed // auto-destroy after this
Rewarded-only callbacks
| Callback | Type | Description |
|---|---|---|
onAdShowed |
void Function()? |
Ad shown |
onUserEarnedReward |
void Function()? |
Fires once when the user has earned the reward (no payload — host grants its own reward) |
onAdDismissed |
void Function()? |
Ad closed (auto destroy after this) |
Complete example
final ad = NovvyRewardedAd(adUnitId: 'your_ad_unit_id');
var earned = false;
ad.onAdLoaded = () { /* ready — enable your "watch ad" entry point */ };
ad.onUserEarnedReward = () => earned = true;
ad.onAdDismissed = () {
if (earned) grantReward(); // grant the reward
ad.destroy();
};
ad.onAdFailedToLoad = (e) => ad.destroy();
ad.load(); // pre-load early
// Show only when the user taps "watch ad":
void onWatchAdTapped() {
if (ad.isReady) ad.show();
}
destroyruns automatically after dismissal; if you abandon the ad before showing it, callad.destroy()manually.
Interstitial Ads — NovvyInterstitialAd #
Same "one-shot, full-screen" shape as rewarded, without onUserEarnedReward.
Flow
NovvyAds.setContentContext(...) // drama scenarios only
↓
NovvyInterstitialAd(adUnitId)
↓
load() → onAdLoaded // ready (or onAdFailedToLoad)
↓
show() → onAdShowed // show at a transition point
↓
onAdDismissed // auto-destroy after this
Callbacks: onAdShowed / onAdDismissed.
Complete example
final ad = NovvyInterstitialAd(adUnitId: 'your_ad_unit_id');
ad.onAdLoaded = () { /* ready — can show at next transition */ };
ad.onAdShowed = () { /* shown */ };
ad.onAdDismissed = () { /* dismissed */ ad.destroy(); };
ad.onAdFailedToLoad = (e) => ad.destroy();
ad.load(); // pre-load early
// At a natural transition (page navigation, level completion, ...):
void onTransition() {
if (ad.isReady) ad.show();
}
destroyruns automatically after dismissal; if you abandon the ad before showing it, callad.destroy()manually.
Mid-roll Ads — NovvyMidAd #
Full-screen, preloadable, shown at a mid-episode cue point. Two-stage: load() → show(). Same callbacks as interstitial (onAdShowed / onAdDismissed).
Flow
NovvyAds.setContentContext(...) // drama scenarios only
↓
NovvyMidAd(adUnitId)
↓
load() → onAdLoaded // ready (or onAdFailedToLoad)
↓
show() → onAdShowed // show at the mid-roll cue
↓
onAdDismissed // auto-destroy after this
Complete example
final ad = NovvyMidAd(adUnitId: 'your_ad_unit_id');
ad.onAdLoaded = ad.show; // or cache and show at the cue point
ad.onAdShowed = () { /* shown */ };
ad.onAdDismissed = () { ad.destroy(); };
ad.onAdFailedToLoad = (e) => ad.destroy();
ad.load();
destroyruns automatically after dismissal; if you abandon the ad before showing it, callad.destroy()manually.
End-roll Ads — NovvyEndAd #
Full-screen, preloadable, shown when an episode finishes. Same shape as mid-roll — only the semantics and ad-unit type differ.
Flow
NovvyAds.setContentContext(...) // drama scenarios only
↓
NovvyEndAd(adUnitId)
↓
load() → onAdLoaded // ready (or onAdFailedToLoad)
↓
show() → onAdShowed // show at episode end
↓
onAdDismissed // auto-destroy after this
Complete example
final ad = NovvyEndAd(adUnitId: 'your_ad_unit_id');
ad.onAdLoaded = ad.show; // or cache and show at episode end
ad.onAdShowed = () { /* shown */ };
ad.onAdDismissed = () { ad.destroy(); };
ad.onAdFailedToLoad = (e) => ad.destroy();
ad.load();
destroyruns automatically after dismissal; if you abandon the ad before showing it, callad.destroy()manually.
Scroll Ads — NovvyScrollAd #
Inline feed ad rendered as a native view embedded in a scrollable list. Lifecycle: load() → embed NovvyScrollAdView → drive setPlaying(true/false) on visibility → destroy().
The optional episodeNumber constructor argument overrides the episode this ad is bound to (e.g. loading episode 2's ad while episode 1 is playing). When non-null, it takes precedence over the setContentContext episode.
Flow
Episode begins playing
↓
NovvyAds.setContentContext(...) // series / episode
↓
NovvyScrollAd(adUnitId, episodeNumber) + load()
↓
onAdLoaded // ready (or onAdFailedToLoad)
↓
Embed NovvyScrollAdView (auto-attaches on platform view creation)
↓
In view: setPlaying(true) → onAdShowed / Out of view: setPlaying(false)
↓
Widget unmounts → auto-detach (not destroy)
↓
Host calls ad.destroy() // release native instance / assets
Class members
| Member | Description |
|---|---|
episodeNumber |
int? constructor arg, overrides the episode in the bid request |
onAdShowed |
Ad became visible in viewport |
onAdHidden |
Ad left viewport / hidden |
onAdPlaybackTick |
void Function(int remainingSeconds)?, fires each second with seconds remaining before skip is allowed (0 = swipeable) |
attach({int? durationSeconds, bool useDefaultSwipeOverlay = true}) |
Attach to a platform view container (normally auto-called by NovvyScrollAdView) |
detach() |
Detach from container |
setPlaying(bool playing) |
true when the item enters the viewport, false when it leaves |
attach() parameters:
durationSeconds— wait time before the user can swipe past (native clamps to a minimum).useDefaultSwipeOverlay— whether to render the SDK's built-in "Swipe to skip" pill. Set tofalseto draw your own UI based ononAdPlaybackTick.
Example
final ad = NovvyScrollAd(adUnitId: 'your_ad_unit_id', episodeNumber: 2);
ad.onAdLoaded = () {
ad.setPlaying(true); // current item is in the viewport
setState(() => _ready = true);
};
ad.onAdHidden = () { /* hidden */ };
ad.onAdPlaybackTick = (remaining) { /* update custom countdown UI */ };
ad.onAdFailedToLoad = (e) => ad.destroy();
ad.load();
Embed the widget (wrap in a SizedBox with the intended height):
if (_ready)
NovvyScrollAdView(ad: ad),
NovvyScrollAdViewauto-attaches when the platform view is created and auto-detaches ondispose. The host still needs to callad.destroy()once the ad is finished to release the native instance.
Pause Ads — NovvyPauseAd #
Realtime pause card (fixed 260×90 logical pixels), shown while the video player is paused. No load(), no onAdLoaded — the request fires in realtime when NovvyPauseAdView is mounted. The host must implement NovvyPlayerAdapter so the SDK can read player state.
⚠️ Mount the widget the moment the video is paused (this immediately fires a request); call
destroy()on resume and re-create the ad next pause.
Flow
Episode begins playing
↓
User pauses the video
↓
NovvyAds.setContentContext(...)
↓
Embed NovvyPauseAdView (auto-attaches, fires realtime request)
↓
onAdShowed // pause card visible
↓
Playback resumes → onAdHidden
↓
destroy() // host must release explicitly
Class members
| Member | Description |
|---|---|
onAdShowed |
Pause card shown |
onAdHidden |
Card hidden (playback resumed) |
onAdFailedToLoad |
Request failed (there is no onAdLoaded) |
attach(NovvyPlayerAdapter player) |
Fire the realtime request (normally auto-called by NovvyPauseAdView). Reads player state once for the request and then pushes state every 500 ms so the SDK can detect resume |
detach() |
Stop pushing state and remove |
NovvyPauseAd.cardWidth / cardHeight |
260 / 90 (logical pixels) |
NovvyPauseAd.viewType |
'ai.novvy.novvy_ads/pause_ad' (internal) |
shownNotifier |
ValueNotifier<bool> used by the widget to toggle touch pass-through (internal) |
Player adapter
The host implements NovvyPlayerAdapter to bridge its own player:
class MyPlayerAdapter extends NovvyPlayerAdapter {
final VideoPlayerController controller;
MyPlayerAdapter(this.controller);
@override
int get currentPositionMs => controller.value.position.inMilliseconds;
@override
bool get isPlaying => controller.value.isPlaying;
}
| Member | Type | Description |
|---|---|---|
currentPositionMs |
int |
Current playback position in milliseconds |
isPlaying |
bool |
true only when actively playing (not paused, buffering, or seeking) |
Both getters are polled at ~500 ms intervals; keep them lightweight (no I/O or locks).
Example
final ad = NovvyPauseAd(adUnitId: 'your_ad_unit_id');
ad.onAdShowed = () { /* pause card shown */ };
ad.onAdHidden = () { /* hidden (playback resumed) */ };
ad.onAdFailedToLoad = (e) { /* ... */ };
// Mount when the video is paused:
NovvyPauseAdView(ad: ad, player: myPlayerAdapter)
// Release on resume:
ad.destroy();
NovvyPauseAdViewis fixed at 260×90 — position it yourself (e.g. inside aStackorAlign). The widget auto-attaches on platform view creation and auto-detaches on unmount.
Banner Ads — NovvyBannerAd #
A persistent banner slot that rotates creatives on a timer: for as long as the widget is mounted, the SDK fills it with one creative after another until the host calls destroy().
Three things set it apart from every other format — check them before integrating:
- There is no
load(). The native banner takes its container in its constructor, and the container only exists once the platform view has been created — so mountingNovvyBannerAdViewis the first request. Callingload()reports an error throughonAdFailedToLoad. - Callbacks fire once per rotation, not once per ad object. Expect another
onAdLoaded/onAdShowedeveryrefreshIntervalSeconds. A host counting its own impressions relies on that repetition. onAdFailedToLoadis not terminal. The SDK retries with backoff and whatever creative is already on screen stays up. The ad is not destroyed — treat it as a warning, not an end state, and calldestroy()yourself when you are done with the slot.
Banner is also the only format that reports clicks to the host (onAdClicked).
Integration flow
NovvyAds.setContentContext(...)
↓
Mount NovvyBannerAdView (auto-attaches on platform view creation → first request)
↓
onAdLoaded → onAdShowed // rotation 1
↓
(user taps) → onAdClicked // SDK already tracked and opened it
↓
onAdLoaded → onAdShowed // rotation 2, refreshIntervalSeconds later
↓
(a rotation gets no fill) → onAdFailedToLoad // NOT terminal: retries, creative stays
↓
unmount → auto-detach; destroy() // host must release
Sizes
The five IAB tiers, in logical pixels (Android dp / iOS pt). Their aspect ratios are mutually distinct because the backend matches a creative to the slot by ratio — picking the wrong tier gets you no fill, not a stretched ad.
NovvyBannerSize |
Size (logical px) | Notes |
|---|---|---|
standard |
320 × 50 | Classic mobile banner (default) |
large |
320 × 100 | Large banner |
mediumRectangle |
300 × 250 | Medium rectangle |
fullBanner |
468 × 60 | Full banner |
leaderboard |
728 × 90 | Leaderboard |
leaderboardis 728 logical pixels wide — wider than any phone — so it will overflow a portrait layout. Wrap it in aFittedBoxto scale it down, or pick a narrower tier.
Class members
| Member | Description |
|---|---|
onAdLoaded |
A creative is ready (once per rotation) |
onAdShowed |
A creative became visible in the slot (once per rotation) |
onAdClicked |
The user tapped the creative. The SDK has already fired the clicktrack and opened the destination; this is for the host's own analytics |
onAdHidden |
The slot was emptied (e.g. a render failure between rotations) |
onAdFailedToLoad |
Request failed — not terminal (see point 3 above) |
size |
NovvyBannerSize, passed at construction and immutable afterwards (the native constructor fixes it; changing tier means a new instance) |
refreshIntervalSeconds |
Seconds between rotations, default 60. Clamped natively to [30, 120]; <= 0 disables rotation and leaves a single creative in the slot |
attach() |
Fire the request and start rotating (normally auto-called by NovvyBannerAdView) |
detach() |
Stop rotating and remove from the container |
pixelSize |
Size — the current tier in logical pixels |
NovvyBannerAd.sizesInLogicalPixels |
Map<NovvyBannerSize, Size> — the table above |
NovvyBannerAd.defaultRefreshIntervalSeconds |
60 (matches the native default) |
NovvyBannerAd.viewType |
'ai.novvy.novvy_ads/banner_ad' (internal) |
shownNotifier |
ValueNotifier<bool> the widget uses to toggle hit-testing (internal) |
Example
final ad = NovvyBannerAd(
adUnitId: 'your_ad_unit_id',
size: NovvyBannerSize.standard, // 320×50
refreshIntervalSeconds: 60, // clamped to [30, 120]
);
ad.onAdShowed = () { /* this rotation's creative is up */ };
ad.onAdClicked = () { /* your own analytics; the SDK already handled the tap */ };
// Not terminal — do NOT destroy here; the SDK retries with backoff.
ad.onAdFailedToLoad = (e) { /* log it */ };
// Mounting is the request:
NovvyBannerAdView(ad: ad)
// When the page goes away:
ad.destroy();
NovvyBannerAdViewis fixed at the tier size — position it yourself. The widget auto-attaches on platform view creation and auto-detaches on unmount.
Event Callback Overview #
Each ad receives events through closures assigned on its instance. All callbacks fire on the main isolate (UI thread).
Full-screen ads (Rewarded / Interstitial / Mid / End):
| Callback | When |
|---|---|
onAdLoaded |
Ad and creative are ready to show |
onAdFailedToLoad(error) |
Load failed; error is a description string |
onAdShowed |
Ad displayed and impression reported |
onAdDismissed |
Ad closed (auto-destroy after this) |
onUserEarnedReward |
Rewarded only — fires once when the ad becomes closable; host grants its own reward |
Inline ads (Scroll / Pause):
| Callback | When |
|---|---|
onAdLoaded |
Ready (Pause has no such callback — realtime request, no preload phase) |
onAdFailedToLoad(error) |
Load failed |
onAdShowed |
Ad attached, visible, impression reported |
onAdHidden |
Ad detached / hidden |
onAdPlaybackTick(remainingSeconds) |
Scroll only — fires every second with the countdown |
Banner slots (Banner):
| Callback | When |
|---|---|
onAdLoaded |
A creative is ready (once per rotation) |
onAdFailedToLoad(error) |
Request failed — not terminal: the SDK retries with backoff, the instance is not destroyed, and any creative already on screen stays up |
onAdShowed |
A creative became visible and its impression was reported (once per rotation) |
onAdClicked |
The user tapped the creative; the SDK has already tracked and opened it |
onAdHidden |
The slot was emptied |
Impression and redirect tracking / billing are all handled internally by the native SDK — the host does not need to intervene. So are clicks, with one exception: the banner slot calls onAdClicked after the SDK has handled the tap, so the host can add its own analytics.
Error Types #
The error argument of onAdFailedToLoad(error) is a String. The Flutter side does not expose the Android SDK's sealed NovvyError — the plugin flattens it before crossing the bridge into these fixed marker strings, which the host can branch on:
error string |
Meaning |
|---|---|
NoFill |
Backend has no fill — normal situation (typically silent, no alerting needed) |
NoBid |
Backend returned no bid |
Timeout |
Request timed out |
InvalidRequest |
Bad request parameters (also: request fired before SDK init) |
NoData |
Empty response body |
EncodingFailed |
Encoding / decoding failure |
InternalError: <details> |
Internal SDK error; colon-suffixed with details |
ad.onAdFailedToLoad = (error) {
if (error == 'NoFill') return; // no fill — normal, silent
if (error.startsWith('InternalError:')) { // has ": <details>" suffix — match by prefix
log('Novvy internal error: $error');
}
ad.destroy(); // downgrade otherwise
};
The first six values are exact matches;
InternalErrorcarries a: <details>suffix, so match withstartsWith('InternalError:').
User & Content Context #
Providing context improves targeting. Set after initialize(); refresh when the content changes. set* replaces (null clears), update* merges per field (null preserves).
NovvyUserContext— User profile; all fields optional. Set once after login and again on identity changes.NovvyContentContext— Current episode. ⚠️ In drama scenarios this is a prerequisite for fill — set the correct series / episode before everyload()/attach().
// User profile: set once after login, again on identity change.
NovvyAds.setUserContext(
NovvyUserContext(
userId: 'user_123',
hashedEmail: NovvyAds.helperHashEmail('user@example.com'),
isPaidUser: false,
age: 25,
gender: 'male',
dramaWatchHistory: [
NovvyDramaRecord(
seriesId: 'S1',
lastWatchedEpisode: 5,
unlockedEpisode: 8,
dramaTitle: 'Series 1',
),
],
maximumAdsInOneDrama: 10,
),
);
// Content context: set before every load / attach so series / episode are correct.
NovvyAds.setContentContext(
NovvyContentContext(
seriesName: 'Series Name',
episodeNumber: 3,
url: 'https://...',
),
);
// Incremental merge (other fields preserved):
NovvyAds.updateUserContext(NovvyUserContext(isPaidUser: true));
NovvyAds.updateContentContext(NovvyContentContext(episodeNumber: 4));
Scroll ads override the episode via the constructor.
NovvyScrollAdtypically preloads the next episode's ad while the current one is playing, so the global context's episode may not match.NovvyScrollAd(episodeNumber: N)overrides only the episode (series / url still come from the context). See Scroll Ads.
NovvyUserContext fields
| Field | Type | Description |
|---|---|---|
userId |
String? |
Publisher-side stable user ID |
hashedEmail |
String? |
SHA-256 hashed email — use NovvyAds.helperHashEmail() to generate |
isPaidUser |
bool? |
Whether the user has an active paid subscription (IAP / IAA) |
age |
int? |
Age; the server maps this to age buckets |
gender |
String? |
"male" / "female" / "unknown" |
dramaWatchHistory |
List<NovvyDramaRecord?>? |
Watched-drama records |
maximumAdsInOneDrama |
int? |
Frequency cap — max ads per drama (default 10) |
NovvyDramaRecord fields
| Field | Type | Description |
|---|---|---|
seriesId |
String |
Unique series identifier |
lastWatchedEpisode |
int |
Last watched episode number |
unlockedEpisode |
int |
Highest unlocked episode number |
dramaTitle |
String? |
Series title (optional) |
NovvyContentContext fields
| Field | Type | Description |
|---|---|---|
seriesName |
String? |
Series name |
episodeNumber |
int? |
Episode number |
url |
String? |
Content URL (maps to native SDK's url field) |
Privacy:
NovvyAds.helperHashEmail(email)trims, lowercases, and SHA-256 hashes the input. Never pass a raw email.
Troubleshooting #
App crashes at launch #
iOS — "app attempted to access privacy-sensitive data without a usage description":
- Add
NSUserTrackingUsageDescriptiontoios/Runner/Info.plist - Run
flutter clean && flutter run
Ads not loading #
onAdFailedToLoad fires with NoFill:
- Backend has no matching fill — this is a normal condition; typically silent
- In drama scenarios, verify
NovvyAds.setContentContext(...)was called with the correct series / episode beforeload()/attach()
onAdFailedToLoad fires with InvalidRequest:
- Verify
appId,endpoint, andapiKeyinNovvyAds.initialize(...) - Ensure
initialize()completed (onComplete(true)) before any adload()/attach() - Confirm the device has an active internet connection
onAdFailedToLoad fires with Timeout:
- The bid request did not return in time — check network conditions and endpoint availability
onAdFailedToLoad fires with InternalError: <details>:
- Inspect the details after the colon and check logcat / Xcode console for stack traces
Android build errors #
minSdk conflict:
# Set minSdk 24 in android/app/build.gradle, then:
flutter clean && flutter pub get
Gradle sync fails:
- Run
./gradlew dependenciesinandroid/to inspect the dependency tree - Ensure
mavenCentral()is in your project-levelrepositories
iOS build errors #
"Unable to find matching slice in '...' for ... x86_64":
- Add the
EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64snippet toios/Podfile(see Configure iOS) - Run
pod installfromios/and rebuild
"Module 'novvy_ads' not found":
- Usually the same root cause as above — verify
EXCLUDED_ARCHSis set in thepost_installhook
pod install fails to download NovvyAds.xcframework.zip:
- Check connectivity to
github.com - Manually download the xcframework and place it under the plugin's
ios/directory; subsequentpod installruns reuse it
iOS Notes #
Simulator architecture #
The NovvyAds xcframework ships ios-arm64 and ios-arm64-simulator slices only. Apple Silicon Macs run the simulator as arm64 and work fine. Intel Macs and Intel-based CI runners cannot run the iOS simulator until the upstream SDK ships an x86_64 slice. Real-device builds work from any Mac.
Additional Resources #
- Example App
- Novvy AI
Support #
Email: founders@novvy.ai
License #
MIT License — Copyright (c) 2026 Novvy AI