Skip to main content

novvy_ads 1.2.1 copy "novvy_ads: ^1.2.1" to clipboard
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 / onAdFailedToLoad closures; 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 / endpoint are 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 every load() / 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 NovvyError class, the Flutter surface passes a plain String error description to onAdFailedToLoad (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();
}

destroy runs automatically after dismissal; if you abandon the ad before showing it, call ad.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();
}

destroy runs automatically after dismissal; if you abandon the ad before showing it, call ad.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();

destroy runs automatically after dismissal; if you abandon the ad before showing it, call ad.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();

destroy runs automatically after dismissal; if you abandon the ad before showing it, call ad.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 to false to draw your own UI based on onAdPlaybackTick.

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),

NovvyScrollAdView auto-attaches when the platform view is created and auto-detaches on dispose. The host still needs to call ad.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();

NovvyPauseAdView is fixed at 260×90 — position it yourself (e.g. inside a Stack or Align). The widget auto-attaches on platform view creation and auto-detaches on unmount.


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:

  1. 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 mounting NovvyBannerAdView is the first request. Calling load() reports an error through onAdFailedToLoad.
  2. Callbacks fire once per rotation, not once per ad object. Expect another onAdLoaded / onAdShowed every refreshIntervalSeconds. A host counting its own impressions relies on that repetition.
  3. onAdFailedToLoad is 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 call destroy() 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

leaderboard is 728 logical pixels wide — wider than any phone — so it will overflow a portrait layout. Wrap it in a FittedBox to 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();

NovvyBannerAdView is 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; InternalError carries a : <details> suffix, so match with startsWith('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 every load() / 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. NovvyScrollAd typically 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 NSUserTrackingUsageDescription to ios/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 before load() / attach()

onAdFailedToLoad fires with InvalidRequest:

  • Verify appId, endpoint, and apiKey in NovvyAds.initialize(...)
  • Ensure initialize() completed (onComplete(true)) before any ad load() / 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 dependencies in android/ to inspect the dependency tree
  • Ensure mavenCentral() is in your project-level repositories

iOS build errors #

"Unable to find matching slice in '...' for ... x86_64":

  • Add the EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64 snippet to ios/Podfile (see Configure iOS)
  • Run pod install from ios/ and rebuild

"Module 'novvy_ads' not found":

  • Usually the same root cause as above — verify EXCLUDED_ARCHS is set in the post_install hook

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; subsequent pod install runs 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 #

Support #

Email: founders@novvy.ai

License #

MIT License — Copyright (c) 2026 Novvy AI

0
likes
140
points
562
downloads

Documentation

API reference

Publisher

verified publishernovvy.ai

Weekly Downloads

Flutter plugin for Novvy Ads SDK.

Homepage

License

MIT (license)

Dependencies

crypto, flutter, meta, plugin_platform_interface

More

Packages that depend on novvy_ads

Packages that implement novvy_ads