View as Markdown

Flutter

The Ezoic Flutter SDK lets you use Ezoic mobile app ads (banner, native, outstream video, instream video, rewarded, and interstitial) from Flutter while relying on the native Android and iOS SDKs for ad loading, Prebid, Google Ad Manager, consent handling, pageview tracking, and remote configuration.

The Flutter package is a plugin bridge over the native SDKs. It does not reimplement the ad stack in Dart.

Requirements

  • Flutter 3.19 or higher
  • Dart 3.3 or higher
  • Android SDK 24 or higher for Android apps
  • iOS 15.0 or higher and Xcode 26.0 or higher for iOS apps
  • Google Mobile Ads application ID provided by Ezoic
  • Native Android and iOS Ezoic SDK access for the platforms you support

Installation

The plugin is distributed from git (it is not on pub.dev). Pin a release tag in your app's pubspec.yaml:

dependencies:
  ezoic_flutter_sdk:
    git:
      url: https://github.com/ezoic/flutter-sdk.git
      ref: v1.13.2
The current Flutter SDK version is 1.13.2, which uses native SDK 1.13.2. Flutter SDK 1.11.1 and earlier pin native SDK 1.11.x and have no consent dialog or screen labels; upgrade to 1.13.0. See Privacy and Consent for what changes when you upgrade.

For iOS, set the iOS 15 platform in your app's ios/Podfile, then run pod install in ios/ (or let flutter run do it):

platform :ios, '15.0'

The native SDK and Google Mobile Ads ship as static binaries, so the plugin must link statically. Its podspec declares static_framework = true, so this works with the template's plain use_frameworks!.

For Android, the native SDK is resolved from Maven Central; nothing else to add.

Platform Setup

Flutter apps still need the native platform setup required by Android and iOS.

Android

Follow the Android setup requirements in the Android guide, including:

  • Making sure Gradle can resolve the Ezoic Android SDK dependency
  • Adding the Google Mobile Ads application ID to AndroidManifest.xml
  • Adding the advertising ID permission if your app targets Android 12 or higher and uses the advertising ID

The Google Mobile Ads application ID is usually provided by Ezoic and can be found in your Ezoic dashboard.

iOS

Follow the Required App Setup for Ads to Serve in the iOS guide. These steps live in your app's Info.plist (and on your website) and directly affect fill, so complete all of them:

  • Installing the Ezoic iOS SDK version provided for your app
  • Adding the Google Mobile Ads application ID (GADApplicationIdentifier) and GADIsAdManagerApp to Info.plist
  • Adding NSUserTrackingUsageDescription to Info.plist (the SDK presents the App Tracking Transparency prompt for you before loading ads — biggest impact on fill)
  • Adding the SKAdNetworkItems identifiers to the app's Info.plist (Apple does not read these from frameworks/SDKs)

The Google Mobile Ads application ID is usually provided by Ezoic and can be found in your Ezoic dashboard.

app-ads.txt

For both platforms, host an app-ads.txt file at the root of the developer website listed on your app's store page (for example, https://example.com/app-ads.txt). It authorizes the buyers that may sell your inventory; a missing or incomplete file causes most programmatic demand to be filtered out. Ezoic provides the required entries — confirm the file is published and current.

Initialize the SDK

Initialize Ezoic before rendering ads.

import 'package:ezoic_flutter_sdk/ezoic_flutter_sdk.dart';

await EzoicAds.initialize(
    const EzoicConfiguration(
        domain: 'example.com',
        debugEnabled: false,
        testMode: false
    ),
);

domain must match the domain configured for your site in Ezoic. The native Android and iOS SDKs both authenticate using the configured domain plus the app's bundle/package identifier — there is no client-side API key.

EzoicConfiguration field Default Meaning
domain required Your domain as configured in the Ezoic dashboard
autoReadConsent true Read IABTCF_* / IABGPP_* consent written by a CMP
subjectToCOPPA false Treat the user as subject to COPPA
requestATTBeforeAds true iOS only: show the App Tracking Transparency prompt before ads
debugEnabled false Verbose native logging
testMode false Prebid debug and $0.00 Ezoic test ads on no-demand auctions (debug builds and simulators only)
autoTrackPageviews true Let the native SDK record pageviews for the host screen on its own. See Pageview Tracking
cmpEnabled true Use the built-in TCF 2.4 consent dialog in GDPR regions. See Privacy and Consent
autoPresentConsent true After initialize succeeds, present the consent dialog once if it is required

initialize throws a PlatformException if the native SDK fails to start.

Add a Banner Ad

Render EzoicBannerView where the banner should appear.

EzoicBannerView(
    adUnitIdentifier: '12345',
    size: EzoicBannerSize.mediumRectangle,
    collapseOnNoFill: true,
    onLoad: () => print('Ezoic banner loaded'),
    onError: (error) => print('Ezoic banner failed: ${error.message}'),
    onSizeChange: (width, height) =>
        print('Ezoic banner size: ${width}x${height}'),
)

Replace 12345 with your Ezoic ad unit identifier. The native SDKs fetch the Google Ad Manager ad unit, Prebid configuration, targeting values, supported sizes, and refresh interval from Ezoic servers. EzoicBannerView sizes itself to the requested EzoicBannerSize. When no ad fills, it collapses (collapseOnNoFill, default true) so it does not leave a blank gap. If a refresh does not fill, the previous ad stays visible. onSizeChange reports the displayed size, or 0×0 when collapsed.

On Android, the current native SDK expects a numeric Ezoic ad unit identifier. Pass it as a string in Flutter, for example '12345'.

Common banner sizes include:

  • EzoicBannerSize.banner: 320x50
  • EzoicBannerSize.largeBanner: 320x100
  • EzoicBannerSize.mediumRectangle: 300x250
  • EzoicBannerSize.fullBanner: 468x60
  • EzoicBannerSize.leaderboard: 728x90

On Android, the selected size is passed into the native SDK's ad request. On iOS, the native SDK selects the configured ad size from Ezoic remote configuration. EzoicBannerView sizes itself to the requested EzoicBannerSize and collapses on no-fill.

EzoicBannerView supports these event callbacks:

  • onLoad
  • onError
  • onSizeChange
  • onImpression
  • onClick
  • onOpen
  • onClose

Add a Rewarded Ad

Rewarded ads are full-screen ads that grant an in-app reward when the user finishes watching. Unlike banners, they are not widgets: load one imperatively ahead of time (for example, at the start of a level), then present it at a natural break. show() resolves with the earned reward, or null if the user dismissed the ad early.

import 'package:ezoic_flutter_sdk/ezoic_flutter_sdk.dart';

Future<void> runRewardedAd() async {
    try {
        final ad = await EzoicRewardedAd.load('12345');

        // Optional: observe lifecycle events
        ad.onDismissed = () => print('Rewarded ad closed');
        ad.onFailedToShow = (error) => print('Show failed: ${error.message}');

        final reward = await ad.show();
        if (reward != null) {
            print('Earned ${reward.amount} ${reward.type}');
            grantReward(reward.amount);
        }
    } catch (error) {
        print('Rewarded ad failed: $error');
    }
}

Replace 12345 with your Ezoic ad unit identifier (passed as a string). Load and show are separate steps; calling show() before the ad has loaded throws. Rewarded ads are single-use — load a new EzoicRewardedAd for each reward opportunity.

You can pass a reward name when you show the ad. The name appears in your reports.

final reward = await ad.show(rewardName: 'extra life');

The reward type and amount come from the reward configured on the Google Ad Manager rewarded ad unit. The other lifecycle callbacks are onShown, onImpression, onClicked, and onUserEarnedReward — all optional.

Add an Interstitial Ad

Interstitial ads are full-screen ads shown at natural transition points (for example, between levels or screens). Unlike rewarded ads, they grant no reward. Like rewarded ads, they are not widgets: load one imperatively ahead of time, then present it at a natural break. show() resolves when the ad is dismissed, or throws an EzoicInterstitialAdError if it fails to present.

import 'package:ezoic_flutter_sdk/ezoic_flutter_sdk.dart';

Future<void> runInterstitialAd() async {
    try {
        final ad = await EzoicInterstitialAd.load('12345');

        // Optional: observe lifecycle events
        ad.onShown = () => print('Interstitial ad shown');
        ad.onFailedToShow = (error) => print('Show failed: ${error.message}');

        await ad.show();
        print('Interstitial ad closed');
    } catch (error) {
        print('Interstitial ad failed: $error');
    }
}

Replace 12345 with your Ezoic ad unit identifier (passed as a string). Load and show are separate steps; calling show() before the ad has loaded throws. Interstitial ads are single-use and auto-destroy once dismissed — load a new EzoicInterstitialAd for each opportunity.

The other lifecycle callbacks are onImpression, onClicked, and onDismissed — all optional.

Add an Outstream Video Ad

Render EzoicOutstreamAdView where the video should appear. Like EzoicNativeAdView, it is a platform view that fills its parent's constraints, so wrap it in a SizedBox (or another constrained parent) to size it.

SizedBox(
    height: 200,
    child: EzoicOutstreamAdView(
        adUnitIdentifier: '12345',
        onLoad: () => print('Ezoic outstream ad loaded'),
        onError: (error) => print('Ezoic outstream ad failed: ${error.message}'),
        onImpression: () => print('Ezoic outstream ad impression'),
        onClick: () => print('Ezoic outstream ad clicked'),
        onOpen: () => print('Ezoic outstream ad opened an overlay'),
        onClose: () => print('Ezoic outstream ad overlay closed'),
        onSizeChange: (width, height) =>
            print('Ezoic outstream size: ${width}x${height}'),
    ),
)

Replace 12345 with your Ezoic ad unit identifier (passed as a string). The native SDKs load the ad and render it inline through Google Ad Manager at the size configured on the server.

EzoicOutstreamAdView fills its parent's constraints and destroys its underlying ad with the platform view's lifecycle — no manual destroy() call is required.

EzoicOutstreamAdView supports these event callbacks:

  • onLoad
  • onError — receives an EzoicOutstreamAdError with message and code
  • onSizeChange
  • onImpression
  • onClick
  • onOpen
  • onClose

EzoicOutstreamAdView also accepts collapseOnNoFill (default true) and collapses on no-fill the same way as EzoicBannerView.

On Android, the current native SDK expects a numeric Ezoic ad unit identifier. Pass it as a string in Flutter, for example '12345'.

Add an Instream Video Ad

EzoicInstreamAd is a view-less controller for instream video. Instream video runs inside your app's OWN video content: your app owns the video player and the Google IMA SDK. The controller renders nothing — its deliverable is a Google Ad Manager VAST ad-tag URL you feed to your IMA AdsRequest. Unlike EzoicInterstitialAd, it is multi-use — it is not auto-destroyed, so keep the instance and call load again for the next video.

import 'package:ezoic_flutter_sdk/ezoic_flutter_sdk.dart';

final instream = EzoicInstreamAd('12345');

Future<void> runInstreamAd() async {
    try {
        final tagUrl = await instream.load(contentUrl: playingVideoUrl);
        // Feed tagUrl to your IMA AdsRequest.adTagUrl and request the preroll.
    } catch (error) {
        print('Instream ad failed: $error');
    }
}

// When IMA reports an ad error, walk the floor waterfall to the next tag.
// A null result means the waterfall is exhausted — give up on the preroll.
final next = await instream.getNextAdTagUrl();

// When IMA reports the ad STARTED, fire the Ezoic impression pixel.
await instream.reportImpression();

// Release the controller when the ad unit is no longer needed.
await instream.destroy();

Replace 12345 with your Ezoic ad unit identifier (passed as a string). load({String? contentUrl}) resolves with the tag URL or throws an EzoicInstreamAdError; pass the URL of the video you're playing so it can be folded into the tag for contextual targeting. getNextAdTagUrl() resolves to null once the waterfall is exhausted. reportImpression({double? revenueUsd}) records the impression on the IMA STARTED event; revenueUsd is optional. Call destroy() only when you're done with the ad unit — the controller keeps its state across loads so you can reuse it for the next video.

Add a Native Ad

Native ads deliver ad assets (headline, icon, advertiser, media, body text, and call to action) rendered in a template designed to match the look and feel of your app content, rather than in a fixed banner or full-screen format. Unlike interstitial and rewarded ads, native ads are widgets: render EzoicNativeAdView where the ad should appear in your layout.

SizedBox(
    height: 320,
    child: EzoicNativeAdView(
        adUnitIdentifier: '12345',
        onLoad: () => print('Ezoic native ad loaded'),
        onError: (error) => print('Ezoic native ad failed: ${error.message}'),
        onImpression: () => print('Ezoic native ad impression'),
        onClick: () => print('Ezoic native ad clicked'),
        onOpen: () => print('Ezoic native ad opened an overlay'),
        onClose: () => print('Ezoic native ad overlay closed'),
    ),
)

Replace 12345 with your Ezoic ad unit identifier (passed as a string). The native SDKs load the ad and render it in an SDK-built template through a platform view.

EzoicNativeAdView is a platform view and fills its parent's constraints, so wrap it in a SizedBox (or another constrained parent) to set its size.

The plugin destroys the underlying native ad with the platform view's lifecycle — no manual destroy() call is required.

EzoicNativeAdView supports these event callbacks:

  • onLoad
  • onError — receives an EzoicNativeAdError with message and code
  • onImpression
  • onClick
  • onOpen
  • onClose
On Android, the current native SDK expects a numeric Ezoic ad unit identifier. Pass it as a string in Flutter, for example '12345'.

The native SDKs include an IAB TCF 2.4 consent management platform. It only acts for users in GDPR regions; elsewhere no dialog is shown and ads load as before.

The dialog is used only when both of these are true:

  • Ezoic has enabled the consent dialog for your domain. This is a server-side setting.
  • cmpEnabled is true in EzoicConfiguration. This is the default.
If your app already runs another CMP (for example UMP or OneTrust), you must set cmpEnabled: false. See Using your own CMP.

The dialog is presented for you. With the default autoPresentConsent: true, the plugin calls presentConsentIfRequired() once, right after initialize succeeds. The initialize future completes first, so your post-init code runs and the dialog appears over it. The outcome is only logged (when debugEnabled is on). Outside GDPR regions, with cmpEnabled: false, when another CMP is present, or when you called setGDPRConsent before initialize, it does nothing.

If there is no foreground Activity or view controller at that moment, the plugin skips the automatic presentation. Call presentConsentIfRequired() yourself later in that case.

You must also add a "Privacy settings" button or menu item that is always reachable and calls presentConsentSettings(). TCF policy requires users to be able to reopen the dialog and withdraw consent at any time.

TextButton(
  onPressed: EzoicAds.presentConsentSettings,
  child: const Text('Privacy settings'),
)
Upgrading from 1.11.x: cmpEnabled and autoPresentConsent default to true, so GDPR-region users now see the built-in dialog after initialize and ad loads wait for their decision. If you pass consent yourself, call setGDPRConsent before initialize on every launch, or set cmpEnabled: false. See Setting consent manually.

Choosing when the dialog appears

Set autoPresentConsent: false and call presentConsentIfRequired() when you are ready, for example from your first screen:

await EzoicAds.initialize(const EzoicConfiguration(
  domain: 'example.com',
  autoPresentConsent: false,
));

final outcome = await EzoicAds.presentConsentIfRequired();
switch (outcome) {
  case Decided(:final decision):
    debugPrint('User chose ${decision.name}');
  case Failed(:final code, :final message):
    debugPrint('Consent UI failed ($code): $message');
  case NotRequired() || AlreadyDecided() || Dismissed() || AlreadyPresenting():
    break;
}

It is safe to call before initialize finishes (it waits for the init response; if init fails you get Failed), and calling it again is harmless: you get AlreadyPresenting while a dialog is in flight and AlreadyDecided once a decision is stored. Present again whenever isConsentRequired() is true and no decision has been made (for example after Dismissed or Failed).

presentConsentIfRequired() and presentConsentSettings() always complete with an EzoicConsentOutcome; they do not throw for consent results:

Outcome When
NotRequired() GDPR doesn't apply, the built-in CMP is disabled (cmpEnabled: false or by Ezoic), another CMP owns consent, or consent is managed by the app (see Setting consent manually)
AlreadyDecided() A still-valid decision is stored; no dialog shown
Decided(decision) The user chose EzoicConsentDecision.acceptAll, rejectAll or custom; the choice is saved
Dismissed() The dialog closed without a choice; ads stay gated for this session. This can happen without user action (the dialog failed to start, or resetConsent() ran while it was loading or open)
AlreadyPresenting() A consent dialog is already on screen, or one is being prepared with nothing on screen yet
Failed(code, message) The dialog couldn't be shown (for example a network error). code is the native EzoicError code, or -1 when the plugin had no foreground Activity or view controller (or the platform call itself failed)

Other consent calls:

  • presentConsentSettings() always reopens the dialog in GDPR regions, even if the user already decided. It returns NotRequired outside GDPR regions, when cmpEnabled is false, when another CMP is present, or when consent is managed by the app.
  • isConsentRequired() (Future<bool?>) is null until the init request completes, or when the server sent no consent information. It is true whenever GDPR applies and the built-in CMP is in charge (including after the user has decided) and false otherwise.
  • resetConsent() deletes the stored decision so the dialog shows again. Ads are gated again until the user decides. Keys written by another CMP are left alone.

Decisions are stored as standard IABTCF_* keys (UserDefaults on iOS, default SharedPreferences on Android), so Prebid, Google Ad Manager, and other IAB-aware SDKs read them as usual.

In GDPR regions, while the built-in CMP is in charge and no decision is stored, ad loads wait for the user's decision:

  • While the consent dialog is loading or on screen, ad loads wait up to 5 minutes in total.
  • While no dialog is in progress, ad loads wait up to 10 seconds.

If the wait runs out, the ad load fails with EzoicErrorCode.consentRequired (5001).

Ads proceed without a decision only when the native dialog itself can't be shown (a Failed outcome): ads then carry IABTCF_gdprApplies=1 and no TC string, which Google treats as limited ads and many Prebid bidders skip. Two Failed codes are different: 1001 (not initialized) writes nothing, and the plugin's -1 (no foreground Activity or view controller) never reaches the CMP. After either, ads stay gated and fail with 5001; call presentConsentIfRequired() again once a screen is showing.

Handling error 5001

EzoicErrorCode.consentRequired (5001) is reported when an ad load fails because GDPR applies and the user has not made a consent choice yet. It arrives:

  • As code on EzoicBannerError, EzoicNativeAdError, and EzoicOutstreamAdError (the ad views' onError callbacks).
  • As code on the EzoicInstreamAdError thrown by EzoicInstreamAd.load.
  • As PlatformException.details when EzoicInterstitialAd.load or EzoicRewardedAd.load fails. (EzoicInterstitialAdError is only thrown by show().)
// Ad views
onError: (error) {
  if (error.code == EzoicErrorCode.consentRequired) {
    // Ask for consent again, e.g. EzoicAds.presentConsentIfRequired().
  }
},

// Interstitial / rewarded loads
try {
  final ad = await EzoicInterstitialAd.load('12345');
  await ad.show();
} on PlatformException catch (e) {
  if (e.details == EzoicErrorCode.consentRequired) {
    // Consent is still required.
  }
}

PlatformException comes from package:flutter/services.dart.

Using your own CMP

Set cmpEnabled: false. The native SDKs then read your CMP's IABTCF_* and IABGPP_* keys as before. The built-in CMP also stands down automatically if it finds IABTCF_CmpSdkID set to another CMP's ID.

await EzoicAds.initialize(const EzoicConfiguration(
  domain: 'example.com',
  cmpEnabled: false,
));

For how each platform reads consent signals, see the Android privacy section and iOS privacy section.

Manual consent and the built-in CMP are mutually exclusive: once setGDPRConsent is called, or when autoReadConsent is false, the built-in CMP doesn't gate ad loads, show its dialog, or write IABTCF_* keys.

The setGDPRConsent value lasts for the current process only. Call setGDPRConsent before EzoicAds.initialize on every launch, or set cmpEnabled: false. Otherwise each cold start begins with the built-in CMP in charge until your call lands. The SDK does not write your consent to IABTCF_* keys for Google Mobile Ads or other SDKs in the app, so your own CMP must write those keys.

await EzoicAds.setGDPRConsent(true, 'TCF_CONSENT_STRING');
await EzoicAds.setGPPConsent('GPP_STRING', '7');
await EzoicAds.setSubjectToCOPPA(false);
await EzoicAds.initialize(const EzoicConfiguration(domain: 'example.com'));

Pageview Tracking

Ezoic reports app traffic per screen, the way it reports a website per URL. Apps have no URLs, so the SDK gives each screen one:

https://<your domain>/<bundle id>/<screen label>

That URL is what per-page reporting and placement optimization key on, so the screen label is what you will see in the Ezoic dashboard.

Automatic tracking

The native SDKs track pageviews automatically for native screens (Activities and view controllers), but a Flutter app is a single native screen (MainActivity / FlutterViewController), so automatic pageviews only carry that host label.

Labeling screens

Label your routes with EzoicAds.trackPageview(screen), for example from a NavigatorObserver:

class EzoicPageviewObserver extends NavigatorObserver {
  void _track(Route<dynamic>? route) {
    final name = route?.settings.name;
    if (route is PageRoute && name != null) EzoicAds.trackPageview(name);
  }

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) =>
      _track(route);

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
      _track(newRoute);

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (route is PageRoute) _track(previousRoute);
  }
}

MaterialApp(
  navigatorObservers: [EzoicPageviewObserver()],
  // ...
);

Labels are free text. Use / for hierarchy (members/profile); spaces become -, punctuation is dropped, and case is kept. The label is also attached to every ad request on that screen, so impressions and pageviews report under the same page. A labeled pageview takes precedence over the automatic one for the same navigation. trackPageview() without a label (or with an empty one) records an unlabeled pageview.

Manual tracking only

If you label every screen yourself, set autoTrackPageviews: false so the native SDKs never record host-screen pageviews on their own:

await EzoicAds.initialize(const EzoicConfiguration(
  domain: 'example.com',
  autoTrackPageviews: false,
));

Troubleshooting

SDK Not Initializing

  1. Confirm the configured domain matches your Ezoic dashboard.
  2. Confirm the native Android/iOS Ezoic SDK dependencies are installed for the platform you are building.
  3. Enable debugEnabled: true and review platform logs.

Ads Not Loading

  1. Initialize Ezoic before rendering EzoicBannerView or EzoicNativeAdView.
  2. Confirm the Ezoic ad unit identifier is configured in Ezoic.
  3. Confirm the Google Mobile Ads application ID is present in AndroidManifest.xml or Info.plist.
  4. Check the platform-specific setup in the Android guide or iOS guide.
  5. An error code of 5001 (EzoicErrorCode.consentRequired) means the built-in consent dialog is waiting for a decision. See Handling error 5001.