The Flutter plugin for the Novvy Ads SDK.
Features
- Interstitial Ads: Full-screen ads that cover the interface of their host app
- Rewarded Ads: Ads that reward users for watching short videos, with a null-safe reward callback
- Banner Ads: Self-managing inline banner ads with server-driven positioning and player-state scheduling
- Feed Ads: Native or H5-zip feed ads with visibility-driven playback control for scrollable lists
- AdMob Mediation: Seamless integration with Google AdMob mediation
- Zero lifecycle boilerplate: The plugin manages the full load → show → dispose cycle internally for all ad types
- Centralized callbacks: Register one
NovvyAdsCallbackat initialization; all ad outcomes are delivered there
Platform Support
- ✅ Android 7.0+ (API 24+)
- ✅ iOS 13.0+ (simulator: Apple Silicon Mac only — see iOS notes)
Installation
1. Add dependency
Add novvy_ads to your pubspec.yaml:
dependencies:
novvy_ads: ^1.0.0-beta.33
2. Get packages
flutter pub get
3. Configure Android
Add AdMob App ID to AndroidManifest
Open android/app/src/main/AndroidManifest.xml and add your AdMob App ID inside the <application> tag:
<manifest>
<application>
<!-- Required by Google Mobile Ads SDK -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/>
</application>
</manifest>
Replace
ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyywith your actual AdMob App ID. The app will crash at launch without this entry.
Verify minSdk
Ensure your app's android/app/build.gradle sets minSdk to at least 24:
android {
defaultConfig {
minSdk 24
}
}
4. Configure iOS
Verify deployment target
In ios/Podfile, ensure the platform line is iOS 13.0 or higher:
platform :ios, '13.0'
Install pods
From your app's ios/ directory:
pod install
The first pod install automatically downloads the NovvyAds iOS SDK
xcframework (~5 MB) into the plugin's ios/ directory; subsequent runs
skip the download unless the pinned SDK version changes.
Add NSUserTrackingUsageDescription to Info.plist
The NovvyAds iOS SDK uses the App Tracking Transparency (ATT) API to
request permission for the IDFA (used for ad personalization and
attribution). iOS will abort the process at launch if you call any
ATT-backed API without declaring NSUserTrackingUsageDescription in
your app's Info.plist.
Open ios/Runner/Info.plist and add:
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
Customize the description string to match your app's tone and any store-review guidance. The exact text is shown to users in the system tracking permission dialog.
Exclude x86_64 for simulator builds
The bundled NovvyAds xcframework only ships arm64 slices, so simulator
builds must drop x86_64 (Intel Mac simulator). The plugin's podspec
already declares this for its own target, but to keep the host app
project in sync — especially on Xcode 15+ where user_target_xcconfig
can be ignored — add the following to your ios/Podfile:
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
# NovvyAds xcframework only ships arm64 simulator slices.
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
Apple Silicon Macs only for simulator. Intel Macs and Intel-based CI runners cannot run the iOS simulator until the upstream NovvyAds SDK ships an
x86_64simulator slice. Real-device builds (arm64) work from any Mac. See iOS notes for context.
Usage
1. Implement NovvyAdsCallback
Create a class that implements the callback interface. All ad outcomes — success or failure — are delivered here. You only need to wire this up once for your entire app.
import 'package:novvy_ads/novvy_ads.dart';
class MyAdsCallback implements NovvyAdsCallback {
@override
void onInterstitialDismissed(String adUnitId) {
print('Interstitial dismissed: $adUnitId');
}
@override
void onInterstitialFailed(String adUnitId, String error) {
print('Interstitial failed [$adUnitId]: $error');
}
@override
void onRewardedDismissed(String adUnitId, NovvyReward? reward) {
if (reward != null) {
print('User earned reward: ${reward.amount} ${reward.type}');
// Grant the reward here (coins, lives, unlocked content, etc.)
} else {
print('Rewarded ad dismissed without completing');
}
}
@override
void onRewardedFailed(String adUnitId, String error) {
print('Rewarded ad failed [$adUnitId]: $error');
}
// Optional — override only if you need banner event tracking
@override
void onBannerImpression(String adUnitId) {
print('Banner impression: $adUnitId');
}
@override
void onBannerFailed(String adUnitId, String error) {
print('Banner failed [$adUnitId]: $error');
}
@override
void onBannerClosed(String adUnitId) {
print('Banner closed by user: $adUnitId');
}
}
2. Initialize the SDK
Call NovvyAds.initialize() once in your main() function, before runApp():
import 'package:flutter/widgets.dart';
import 'package:novvy_ads/novvy_ads.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await NovvyAds.initialize(
config: NovvyInitConfig(
appId: 'YOUR_APP_ID',
endpoint: 'YOUR_END_POINT',
apiKey: 'YOUR_API_KEY',
),
callback: MyAdsCallback(),
);
runApp(const MyApp());
}
3. Set User & Content Context (Recommended)
To improve ad targeting and revenue, pass user- and content-level context to the SDK before showing ads. Call this whenever the user signs in, the user profile changes, or the user navigates to new content:
NovvyAds.updateContext(
NovvyContextConfig(
// ── User attributes (privacy-sensitive) ──────────────────────────
userId: 'your-publisher-side-user-id',
hashedEmail: NovvyAds.hashEmail('your_email@example.com'),
isPaidUser: true,
age: 28,
// ── Content attributes ───────────────────────────────────────────
seriesName: 'YOUR_SERIES_NAME',
episodeNumber: 1,
contentUrl: 'YOUR_CONTENT_URL',
),
);
All fields are independently optional — pass any combination, or none. Each field maps directly to the OpenRTB bid request the SDK sends to the Novvy bid server:
| Field | Sent as | What to pass |
|---|---|---|
userId |
user.user_id |
Your publisher-side stable user identifier (any string). Use the same value you store on your backend so server-side reporting can be reconciled. |
hashedEmail |
user.hashed_email |
The user's email address, already SHA-256 hashed. Always pipe through NovvyAds.hashEmail(...) — never pass a raw email. |
isPaidUser |
user.is_paid_user |
true if the current user has an active paid subscription / premium entitlement, false otherwise. Used for paid-vs-free segmentation. |
age |
user.age |
The user's age in years (an integer such as 28). Used for demographic targeting. |
seriesName |
app.content.series_name |
The current series / show / channel the user is watching. |
episodeNumber |
app.content.episode_number |
The episode number within that series. |
contentUrl |
app.content.url |
The canonical URL of the current content page (deep link or web URL). |
Tip:
NovvyAds.hashEmail(rawEmail)returns a SHA-256 hex digest. Always hash on the client — never pass raw email addresses to the SDK.
When does it take effect? Context is read lazily at each ad's
load()call, so changes only apply to ads loaded after you callupdateContext. Set it as early as you have the data; it's safe to call multiple times.
Note:
userId,isPaidUser, andageare currently forwarded on Android only; iOS support lands once the iOS native SDK ships the fields.hashedEmailand the content fields work on both platforms today.
4. Interstitial Ads
Show a full-screen interstitial at a natural transition point (e.g. between episodes):
import 'package:novvy_ads/novvy_ads.dart';
class EpisodeScreen extends StatefulWidget {
const EpisodeScreen({super.key});
@override
State<EpisodeScreen> createState() => _EpisodeScreenState();
}
class _EpisodeScreenState extends State<EpisodeScreen> {
@override
void initState() {
super.initState();
NovvyAds.updateContext(NovvyContextConfig(
seriesName: 'My Series',
episodeNumber: 3,
));
}
void _onEpisodeEnd() {
NovvyAds.showInterstitial('YOUR_AD_UNIT_ID');
// Result arrives in MyAdsCallback.onInterstitialDismissed
// or MyAdsCallback.onInterstitialFailed
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _onEpisodeEnd,
child: const Text('Next Episode'),
);
}
}
The plugin manages the full lifecycle (load → show → dispose) internally.
5. Rewarded Ads
Offer users a reward in exchange for watching a video:
void _onUnlockContent() {
NovvyAds.showRewarded('YOUR_REWARDED_AD_UNIT_ID');
// Result arrives in MyAdsCallback.onRewardedDismissed or onRewardedFailed
}
Important:
onRewardedDismissedis always called when the ad closes. Only grant the reward whenrewardis non-null — a null value means the user dismissed the ad early without completing the video.
6. Banner Ads
Banner ads are self-managing inline native views that render inside your widget tree. Like interstitial and rewarded ads, the plugin handles the full lifecycle (load → attach → show/hide → dispose) internally.
The SDK supports two server-driven display modes — no client-side code changes are needed to switch between them:
| Mode | Behavior | Use case |
|---|---|---|
| Playback Position | Banner appears/disappears at specific playback timestamps configured on the server | Mid-roll or post-roll overlay ads timed to content |
| Play / Pause | Banner appears when the player is paused and hides when playback resumes | Non-intrusive ads that only show during user-initiated pauses |
The display mode is controlled entirely by the server configuration for each ad unit. Your client integration is the same for both modes — just implement
NovvyPlayerAdapterand the SDK handles the rest.
Implement a NovvyPlayerAdapter
The SDK uses the adapter to read your player's current state. Implement this interface to bridge your video/audio player:
import 'package:novvy_ads/novvy_ads.dart';
class MyPlayerAdapter extends NovvyPlayerAdapter {
final MyVideoPlayer _player;
MyPlayerAdapter(this._player);
@override
int get currentPositionMs => _player.position.inMilliseconds;
@override
bool get isPlaying => _player.isPlaying;
}
currentPositionMs— used by the Playback Position mode to evaluate show/hide time windows.isPlaying— used by the Play / Pause mode to toggle banner visibility.
Both properties are polled by the plugin (~500 ms interval) and forwarded to the native SDK, so keep the getters lightweight.
Display the banner
Place NovvyAds.banner() inside a Stack. The widget manages loading, positioning, and cleanup automatically:
import 'package:novvy_ads/novvy_ads.dart';
class _MyPageState extends State<MyPage> {
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Your content
MyContentWidget(),
// Banner — auto-positioned by server config (top/bottom + offset)
NovvyAds.banner(
adUnitId: 'YOUR_BANNER_AD_UNIT_ID',
player: MyPlayerAdapter(myVideoPlayer),
),
],
);
}
}
The banner has a fixed width of 260dp × 90dp, left-aligned within the container, and auto-positioned based on server-driven gravity (top/bottom) and vertical offset. It does not block touch events on surrounding UI.
Tip: When hidden (regardless of display mode), the widget is fully transparent and does not intercept touch events (
IgnorePointer).
7. Feed Ads
Feed ads are designed to be inserted into scrollable lists (e.g. article feeds, episode lists). Two creative formats are supported and the plugin selects the renderer automatically — host code is identical for both:
- Image creative — rendered natively (hero image + product info overlay).
- H5 zip creative — rendered in a WebView from a downloaded zip bundle (HTML/CSS/JS, optional video). The bundle is downloaded once per
material_urland cached on disk; subsequent loads of the same creative reuse the cache.
Playback is driven by viewport visibility — you control when the ad plays and pauses via setPlaying(true/false).
Step 1 — Add feed ad callbacks
Add the feed-related overrides to your NovvyAdsCallback implementation:
class MyAdsCallback implements NovvyAdsCallback {
// ... existing interstitial/rewarded/banner callbacks ...
@override
void onFeedAdReady(String adUnitId, NovvyFeedAdProvider provider) {
// Ad loaded successfully — store the provider and insert provider.widget
// into your feed list (see Step 3)
}
@override
void onFeedAdFailed(String adUnitId, String error) {
// Ad failed to load — log or retry as needed
print('Feed ad failed [$adUnitId]: $error');
}
@override
void onFeedAdPlaybackTick(String adUnitId, int remainingSeconds) {
// Fires every second while the ad is playing.
// Use this to show a countdown timer overlay if desired.
}
// Optional — override for analytics
@override
void onFeedAdImpression(String adUnitId) { }
@override
void onFeedAdClicked(String adUnitId) { }
}
Step 2 — Request a feed ad
Call loadFeedAd() to start loading. The result is delivered asynchronously via onFeedAdReady / onFeedAdFailed:
NovvyAds.loadFeedAd(
adUnitId: 'YOUR_FEED_AD_UNIT_ID',
durationSeconds: 5, // ad playback duration in seconds (default: 5)
);
Tip: Call
loadFeedAd()ahead of time (e.g. when the page loads) so the ad is ready before the user scrolls to the insertion point.
Step 3 — Insert the ad widget into your feed
When onFeedAdReady fires, store the NovvyFeedAdProvider and insert its widget into your list data source:
NovvyFeedAdProvider? _feedAdProvider;
@override
void onFeedAdReady(String adUnitId, NovvyFeedAdProvider provider) {
setState(() {
_feedAdProvider = provider;
_feedItems.insert(3, provider); // insert at desired position
});
}
In your ListView.builder, render the ad widget wrapped in a fixed-height SizedBox:
ListView.builder(
itemCount: _feedItems.length,
itemBuilder: (context, index) {
final item = _feedItems[index];
if (item is NovvyFeedAdProvider) {
return SizedBox(
height: 250, // adjust to match your feed item height
child: item.widget,
);
}
return NormalFeedItem(item);
},
)
Step 4 — Control playback based on visibility
Feed ads do not auto-play. You must call setPlaying(true) when the ad scrolls into the viewport and setPlaying(false) when it scrolls out. The recommended approach is to use the visibility_detector package:
if (item is NovvyFeedAdProvider) {
return VisibilityDetector(
key: Key('feed-ad-${item.hashCode}'),
onVisibilityChanged: (info) {
item.setPlaying(info.visibleFraction > 0.5);
},
child: SizedBox(
height: 250,
child: item.widget,
),
);
}
The onFeedAdPlaybackTick callback fires every second while playing, providing a remainingSeconds countdown you can use to display a timer.
H5 zip note: When the feed scrolls back into view after a previous
setPlaying(false), the H5 page is reloaded fromindex.htmlso playback restarts from a clean state — the user taps the creative to replay (matches typical TikTok-style feed UX).
Step 5 — Dispose when done
You must call provider.dispose() manually when the ad is no longer needed (e.g. the page is destroyed or the user navigates away). Removing the widget from the tree only detaches the native view — it does not release native resources.
@override
void dispose() {
_feedAdProvider?.dispose();
super.dispose();
}
Note: Feed ads currently support Android only. iOS support is coming soon.
API Reference
NovvyAds
| Method | Description |
|---|---|
initialize({config, callback}) |
Initialize the Novvy SDK. Call once at startup. |
updateContext(NovvyContextConfig) |
Update contextual signals for ad targeting. |
showInterstitial(adUnitId) |
Load and show an interstitial ad. |
showRewarded(adUnitId) |
Load and show a rewarded video ad. |
banner({adUnitId, player}) |
Create a self-managing banner ad widget. |
loadFeedAd({adUnitId, durationSeconds}) |
Load a feed ad. Result delivered via onFeedAdReady. |
hashEmail(email) |
SHA-256 hash an email for privacy compliance. |
NovvyAdsCallback
| Method | Description |
|---|---|
onInterstitialDismissed(adUnitId) |
Interstitial was shown and dismissed. |
onInterstitialFailed(adUnitId, error) |
Interstitial failed to load or show. |
onRewardedDismissed(adUnitId, reward) |
Rewarded ad dismissed; reward is non-null if earned. |
onRewardedFailed(adUnitId, error) |
Rewarded ad failed to load or show. |
onBannerImpression(adUnitId) |
(Optional) A banner ad impression was recorded. |
onBannerFailed(adUnitId, error) |
(Optional) A banner ad failed to load. |
onBannerClosed(adUnitId) |
(Optional) The user closed the banner via the close button. |
onFeedAdReady(adUnitId, provider) |
Feed ad loaded; insert provider.widget into your list. |
onFeedAdFailed(adUnitId, error) |
Feed ad failed to load. |
onFeedAdPlaybackTick(adUnitId, remainingSeconds) |
(Optional) Fires every second with countdown. |
onFeedAdImpression(adUnitId) |
(Optional) A feed ad impression was recorded. |
onFeedAdClicked(adUnitId) |
(Optional) The feed ad was clicked. |
NovvyInitConfig
| Field | Type | Description |
|---|---|---|
appId |
String |
Your application ID |
endpoint |
String |
Novvy API endpoint URL |
apiKey |
String |
Your API key |
NovvyContextConfig
All fields are optional. See Set User & Content Context for usage and OpenRTB field mappings.
| Field | Type | Description |
|---|---|---|
userId |
String? |
Publisher-side stable user identifier (Android only for now) |
hashedEmail |
String? |
SHA-256 hashed user email — use NovvyAds.hashEmail(...) |
isPaidUser |
bool? |
Whether the current user is a paying/subscribed user (Android only for now) |
age |
int? |
The user's age in years, for demographic targeting (Android only for now) |
seriesName |
String? |
Current content series name |
episodeNumber |
int? |
Current episode number |
contentUrl |
String? |
URL of the current content page |
NovvyPlayerAdapter (abstract)
| Property | Type | Description |
|---|---|---|
currentPositionMs |
int |
Current playback position in milliseconds. |
isPlaying |
bool |
Whether the player is currently playing. |
NovvyFeedAdProvider
| Property / Method | Type | Description |
|---|---|---|
widget |
Widget |
The native ad view to insert into your list. Wrap in a SizedBox with a fixed height. |
setPlaying(bool) |
void |
Set playback state — true when visible, false when scrolled out. |
dispose() |
void |
Release all native resources. Must be called manually. |
NovvyReward
| Field | Type | Description |
|---|---|---|
amount |
int |
Reward amount |
type |
String |
Reward type identifier |
Troubleshooting
AdMob configuration
"The Google Mobile Ads SDK was initialized incorrectly" / app crashes on launch:
- Verify the
<meta-data>entry forcom.google.android.gms.ads.APPLICATION_IDis present inAndroidManifest.xml - Confirm the value is your AdMob App ID (starts with
ca-app-pub-)
"Missing APPLICATION_ID" in logcat:
- Ensure the
<meta-data>tag is inside<application>, not<manifest>
Ads not loading
onInterstitialFailed / onRewardedFailed fires immediately:
- Check that
appId,endpoint, andapiKeypassed toNovvyAds.initialize()are correct - Confirm the device has an active internet connection
- Check logcat for more detailed error output from the Novvy SDK
Assertion error at runtime
NovvyAds.initialize() must be called before showInterstitial():
- You called
showInterstitial()orshowRewarded()beforeinitialize()completed - Ensure
await NovvyAds.initialize(...)finishes inmain()beforerunApp()
Android build errors
minSdk conflict:
- Set
minSdk 24(or higher) inandroid/app/build.gradle - Run
flutter clean && flutter pub getand rebuild
Gradle sync fails:
- Run
./gradlew dependenciesinandroid/to inspect the dependency tree - Ensure
mavenCentral()is listed in your project-levelrepositories
iOS build errors
Unable to find matching slice in 'ios-arm64 ios-arm64-simulator' for ... x86_64:
- Add the
EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64snippet to yourios/Podfilepost_install(see Configure iOS) - After editing, run
pod installfromios/and rebuild
Module 'novvy_ads' not found:
- This is usually the same root cause as above — the host app target
is still building for
x86_64. Verify thePodfilepost_installhook ran successfully, and thatEXCLUDED_ARCHSis set inRunner.xcodeprojBuild Settings for every configuration.
App crashes immediately on NovvyAds.initialize() with __abort_with_payload / "app attempted to access privacy-sensitive data without a usage description":
- Your
ios/Runner/Info.plistis missing theNSUserTrackingUsageDescriptionkey. Add it as shown in Configure iOS. - After editing the plist, do a full rebuild —
flutter clean && flutter run— so the new plist is packaged into the app bundle.
Pod install fails to download NovvyAds.xcframework.zip:
- Check internet connectivity to
github.com(the prepare_command script runscurlagainst the GitHub Release asset) - Manually download from the URL printed in the prepare_command failure
and place the unzipped
NovvyAds.xcframeworkdirectly under the plugin'sios/directory; subsequentpod installruns will reuse it
iOS Notes
AdMob mediation on iOS
The Flutter plugin links the NovvyAds Core xcframework, which routes ads directly through Novvy's own delivery stack. Unlike Android — where Google Mobile Ads SDK is bundled and AdMob mediation is transparent — iOS exposes AdMob mediation as a separate concern handled by the host app, not by this plugin.
If your iOS app already uses Google AdMob and you want Novvy to act as
one of its waterfall networks, integrate the official NovvyAds/AdMob
subspec in addition to this plugin:
# ios/Podfile, alongside the Flutter plugin:
pod 'NovvyAds/AdMob', :git => 'https://github.com/NovvyAI/novvy-ads-cocoapods.git', :tag => 'v1.0.0-beta.2'
Then register NovvyAdMobMediationAdapter as a Custom Event in the
AdMob console for each ad unit you want Novvy to bid on. See the
NovvyAds iOS SDK README
for the full mediation setup.
This is independent of NovvyAds.showInterstitial(...) / showRewarded(...)
calls from Dart — those always go through the Novvy direct path regardless.
Simulator architecture limitation
The bundled NovvyAds xcframework currently ships ios-arm64 and
ios-arm64-simulator slices only. Apple Silicon Macs build the
simulator as arm64 and are unaffected; Intel Macs and Intel-based
CI runners cannot run the iOS simulator until upstream provides an
x86_64 simulator slice. Real-device builds work from any Mac.
Requirements
- Dart: 3.0.0+
- Flutter: 3.10.0+
- Android: API 24+ (Android 7.0 Nougat)
- iOS: 13.0+ (Xcode 14.0+, Swift 5.0+)
Additional Resources
Support
For issues or questions:
- Email: founders@novvy.ai
License
MIT License — Copyright (c) 2026 Novvy AI