dartpub.dev DartNative · beta
plugins / google_mobile_ads_kit
go

google_mobile_ads_kit

v0.1.0 MIT

Google Mobile Ads (AdMob) for DartNative

Google Mobile Ads (AdMob) for DartNative — the native Google Mobile Ads SDKs behind the familiar google_mobile_ads API. A banner or native ad is a real AdView (Android) / BannerView (iOS) mounted straight into the native view hierarchy. There is no PlatformView, no texture compositing and no MethodChannel: every call crosses dart:ffi directly, and ad events come back through a single hot-restart-safe dispatcher. ## Familiar API BannerAd, InterstitialAd, RewardedAd, RewardedInterstitialAd, AppOpenAd, NativeAd, AdSize, AdRequest and the preloaders are named and shaped like google_mobile_ads, so most Flutter ad code ports with little more than an import change. The intentional differences are documented in the migration guide.

by cafelafe/google_mobile_ads_kit · DartNative ≥ 3.0 · updated 4 days ago
Install
Free
pubspec.yaml
dependencies:
  google_mobile_ads_kit:
    hosted: https://dartpub.dev
    version: ^0.1.0
Weekly installs
0
Active apps
1
Rating
0.0 · 0
Open issues
0

google_mobile_ads_kit

Google Mobile Ads (AdMob) for DartNative — the native Google Mobile Ads SDKs behind the familiar google_mobile_ads API. iOS and Android.

Why you'll like it

  • Real native ads — a banner or native ad is an AdView / BannerView mounted straight into the native view hierarchy. No PlatformView, no compositing, no MethodChannel: pure dart:ffi.
  • Familiar APIBannerAd, InterstitialAd, RewardedAd, NativeAd, AdSize, AdRequest, the preloaders… named and shaped like google_mobile_ads, so most Flutter code ports unchanged. See Migrating from Flutter.
  • Less code — put BannerAd(...) in the widget tree and it loads itself. No AdWidget, no manual load() / dispose().
  • Every format — banner, native, interstitial, rewarded, rewarded interstitial, app open, and preloading for the four full-screen formats.

Supported ad formats

Format Description Android iOS
App open Full-page ad shown while the app loads or resumes
Banner Fixed sizes and anchored adaptive banners, in the widget tree
Native Built-in small / medium templates, or your own native layout via a factory
Interstitial Full-page ad at natural transitions
Rewarded Video ad that grants an in-app reward
Rewarded interstitial Rewarded ad without an opt-in prompt
Preloading InterstitialAdPreloader, RewardedAdPreloader, RewardedInterstitialAdPreloader, AppOpenAdPreloader
Not yet
🚧 Inline adaptive / collapsible banners Anchored adaptive banners are supported
🚧 Mediation The Next-Gen SDK is AdMob-only
🚧 Ad Manager (GAM) AdManagerBannerAd and friends

Requires iOS 15.0+ / Android minSdk 24. On other platforms (web, desktop) every call is a no-op rather than an error, so shared code keeps running. iOS preloading is backed by the SDK's Beta preloader module, so keep a pollAd == null fallback.

Install

dependencies:
  google_mobile_ads_kit: ^0.1.0   # from dartpub.dev
dn pub get
import 'package:google_mobile_ads_kit/google_mobile_ads_kit.dart';

void main() {
  DartNativePluginRegistrant.registerAll();   // generated by dn pub get
  MobileAds.instance.initialize();            // required; do not await before runApp
  runApp(const MyApp());
}

The plugin registers itself through the generated lib/dartnative_plugin_registrant.dart. initialize() is required — the Android SDK refuses ad requests before it — and the plugin runs it off the main thread for you. The SDK queues requests made during startup.

Quick look

A banner goes straight into the tree. It loads when it mounts and is destroyed when it unmounts:

BannerAd(
  adUnitId: 'ca-app-pub-3940256099942544/6300978111',  // Google test unit
  size: AdSize.banner,
  request: const AdRequest(),
  listener: BannerAdListener(
    onAdLoaded: (ad) => dnLog('loaded'),
    onAdFailedToLoad: (ad, error) => dnLog('failed: $error'),
  ),
)

An interstitial loads with a callback and shows once:

InterstitialAd.load(
  adUnitId: 'ca-app-pub-3940256099942544/1033173712',  // Google test unit
  request: const AdRequest(),
  adLoadCallback: InterstitialAdLoadCallback(
    onAdLoaded: (ad) {
      ad.fullScreenContentCallback = FullScreenContentCallback(
        onAdDismissedFullScreenContent: (ad) => ad.dispose(),
      );
      ad.show();
    },
    onAdFailedToLoad: (error) => dnLog('failed: $error'),
  ),
);

Platform setup

⚠️ Both platforms: set your AdMob App ID natively

DartNative has no manifest-merge step, so you add the App ID to both native projects yourself. Without it the SDK crashes on startup. Find it in the AdMob console under Apps → App settings — it contains ~; ad unit IDs contain /.

ios/Runner/Info.plist:

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-################~##########</string>

android/app/src/main/AndroidManifest.xml, inside <application>:

<meta-data
    android:name="com.google.android.gms.ads.APPLICATION_ID"
    android:value="ca-app-pub-################~##########"/>

On Android the plugin reads this <meta-data> and hands it to the Next-Gen SDK, so the entry is the same as for Flutter. If it is missing, MobileAds.instance.initialize() fails with a message naming it.

iOS

  • Deployment target 15.0+, in both ios/Podfile (platform :ios, '15.0') and the Xcode project's IPHONEOS_DEPLOYMENT_TARGET. Targeting lower fails at pod install with "requires a higher minimum iOS deployment version than your application is targeting".

  • On Xcode 26/27, raise your pods too. Several packages in a DartNative app still declare iOS 12–14 and recent Xcode refuses to build below 15.0 — the build stops at "Target Integrity" before compiling anything. Add this to the post_install block in ios/Podfile (example/ios/Podfile has it in context):

    target.build_configurations.each do |config|
      current = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
      if current.nil? || current.to_f < 15.0
        config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
      end
    end
    
  • Tracking and SKAdNetwork. The SDK links AppTrackingTransparency and AdSupport. Add NSUserTrackingUsageDescription and Google's SKAdNetworkItems to Info.plist per the AdMob iOS quick start.

Android

The plugin targets the GMA Next-Gen SDK (ads-mobile-sdk), which needs minSdk 24+, compileSdk 35+ and Kotlin 1.9+ in your app's android/app/build.gradle(.kts). Nothing else to configure. See Android SDK choice for why there is no legacy switch.

Usage

Banner

Use the AdSize constants (banner, largeBanner, mediumRectangle, fullBanner, leaderboard). A hand-built AdSize(width: 320, height: 50) is not the same request: AdMob reads a custom size as a flexible slot and may fill it with a differently shaped creative.

For an anchored adaptive banner, compute the size from the available width:

LayoutBuilder(
  builder: (context, constraints) {
    final size = AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
      constraints.maxWidth.truncate(),
    );
    if (size == null) return const SizedBox.shrink();   // null off Android/iOS
    return BannerAd(adUnitId: '...', size: size, listener: BannerAdListener(...));
  },
)

That call is synchronous here (it is a pure calculation; FFI has no async mode). The Future-returning AdSize.getAnchoredAdaptiveBannerAdSize(width) exists too, so ported code compiles unchanged.

The creative is drawn at its own size and centred in the slot — AdMob forbids scaling or cropping ads.

Native

A native ad's layout is built natively: AdMob requires every asset view to be registered with the SDK (for click handling and viewability), which cannot be done from Dart. Two ways to supply the layout.

Built-in template — two layouts ship with the plugin, styled from Dart:

NativeAd(
  adUnitId: 'ca-app-pub-3940256099942544/2247696110',  // Google test unit
  nativeTemplateStyle: const NativeTemplateStyle(
    templateType: TemplateType.medium,   // or .small
    mainBackgroundColor: 0xFFFFFFFF,     // 32-bit ARGB, not a dart:ui Color
    cornerRadius: 12,
    callToActionTextStyle: NativeTemplateTextStyle(
      textColor: 0xFFFFFFFF,
      backgroundColor: 0xFF2563EB,
    ),
  ),
  listener: NativeAdListener(
    onAdLoaded: (ad) => dnLog('loaded'),
    onAdFailedToLoad: (ad, error) => dnLog('failed: $error'),
  ),
)

Your own layout — register a factory natively, name it from Dart, and say how tall your layout is. On Android, in MainActivity.onCreate:

GoogleMobileAdsKitPlugin.registerNativeAdFactory(
    this, "adFactoryExample", MyNativeAdFactory(layoutInflater))

On iOS, in AppDelegate.application(_:didFinishLaunchingWithOptions:):

import google_mobile_ads_kit

GMAKMobileAds.registerNativeAdFactory("adFactoryExample", factory: MyNativeAdFactory())

Register under the same id on both platforms; Dart names it once:

NativeAd(
  adUnitId: '...',
  factoryId: 'adFactoryExample',
  height: 120,
  listener: NativeAdListener(),
)

The factory builds a NativeAdView and assigns each asset view (headlineView, iconView, callToActionView, …). That assignment is AdMob policy, not bookkeeping: an unregistered asset is not clickable and records no impression. Android calls registerNativeAd last; on iOS the plugin binds the ad for you once your factory returns, so do not set nativeAd yourself. An id that was never registered fails the load immediately, before a request goes out.

A native ad's height is not known before it loads, so the widget reserves one: the template default (small 144 on iOS / 90 on Android, medium 350) or the height you pass.

Interstitial

See Quick look. An ad shows once. Dispose it when it is spent — after dismissal, and after a failed show — then load another.

Rewarded

The reward callback goes to show and fires only once the user has watched enough. Grant the reward there and nowhere else.

RewardedAd.load(
  adUnitId: 'ca-app-pub-3940256099942544/5224354917',  // Google test unit
  request: const AdRequest(),
  rewardedAdLoadCallback: RewardedAdLoadCallback(
    onAdLoaded: (ad) {
      ad.fullScreenContentCallback = FullScreenContentCallback(
        onAdDismissedFullScreenContent: (ad) => ad.dispose(),
      );
      ad.show(onUserEarnedReward: (ad, reward) => grantCoins(reward.amount));
    },
    onAdFailedToLoad: (error) => dnLog('failed: $error'),
  ),
);

RewardedInterstitialAd has the same shape; AdMob requires you to show a reward announcement before it.

App open

AppOpenAd.load(
  adUnitId: 'ca-app-pub-3940256099942544/9257395921',  // Google test unit
  request: const AdRequest(),
  adLoadCallback: AppOpenAdLoadCallback(
    onAdLoaded: (ad) => _pendingAd = ad,
    onAdFailedToLoad: (error) => dnLog('failed: $error'),
  ),
);

AdMob expires an app open ad four hours after it loads — record the load time and discard a stale one. Show it on a cold start or a return to the foreground, never over content the user is already using.

Preloading

A preloader keeps a small buffer filled in the background, so showing an ad is instant. Start it once, then poll whenever you need one:

await InterstitialAdPreloader.start(
  preloadId: 'level-end',
  preloadConfiguration: const PreloadConfiguration(
    adUnitId: 'ca-app-pub-3940256099942544/1033173712',  // Google test unit
    bufferSize: 2,
  ),
  callback: PreloadCallback(
    onAdPreloaded: (id, responseInfo) => dnLog('ready: $id'),
    onAdsExhausted: (id) => dnLog('empty: $id'),
    onAdFailedToPreload: (id, error) => dnLog('failed: $error'),
  ),
);

final ad = await InterstitialAdPreloader.pollAd('level-end');
if (ad != null) {
  ad.fullScreenContentCallback = FullScreenContentCallback(
    onAdDismissedFullScreenContent: (ad) => ad.dispose(),
  );
  await ad.show();
} else {
  // buffer empty — it refills on its own; fall back to a normal load
}

Keep bufferSize small: a buffered ad that is never shown is a wasted impression opportunity, and AdMob measures that. RewardedAdPreloader, RewardedInterstitialAdPreloader and AppOpenAdPreloader work the same way.

On iOS the preloader lives in the SDK's Beta module (GoogleMobileAds_Private), so its API may change between SDK releases; the podspec's ~> 13.0 pin bounds that. Keep the pollAd == null fallback either way — an empty buffer is normal on both platforms.

Targeting and global settings

  • AdRequest(keywords:, contentUrl:, neighboringContentUrls:, nonPersonalizedAds:, extras:). Set nonPersonalizedAds: true when the user has not consented — obtaining consent is the app's job; this only forwards the decision.
  • MobileAds.instance.setAppMuted(bool).
  • ad.responseInfo, ad.onPaidEvent on every format; setImmersiveMode (Android only) and setServerSideOptions on the rewarded formats.

Banners inside scrolling lists

⚠️ Read this before putting a banner or native ad in a list.

FastList, FastGrid and MasonryFastGrid recycle their cells (they are backed by RecyclerView / UITableView). A recycled ad is unmounted and remounted as it scrolls out and back, and each remount issues a new ad request — wasted inventory, a lower match rate, and an invalid-traffic risk.

Teardown is deferred by one frame so a cell that is recycled straight back in keeps its ad, but a cell that scrolls away and returns is a new request. Prefer a non-recycling container:

SingleChildScrollView(
  child: Column(children: [ ...content, BannerAd(...) ]),
)

If you must use a recycling list, leave keepAliveCount unset.

Test ad units

Never use production ad units during development — it can get your account suspended. Google's always-fill test units:

Format Android iOS
App open ca-app-pub-3940256099942544/9257395921 ca-app-pub-3940256099942544/5575463023
Banner ca-app-pub-3940256099942544/6300978111 ca-app-pub-3940256099942544/2934735716
Native ca-app-pub-3940256099942544/2247696110 ca-app-pub-3940256099942544/3986624511
Interstitial ca-app-pub-3940256099942544/1033173712 ca-app-pub-3940256099942544/4411468910
Rewarded ca-app-pub-3940256099942544/5224354917 ca-app-pub-3940256099942544/1712485313
Rewarded interstitial ca-app-pub-3940256099942544/5354046379 ca-app-pub-3940256099942544/6978759866

Differences from google_mobile_ads

google_mobile_ads google_mobile_ads_kit
Banner / native placement AdWidget(ad: ...) after load() The widget goes directly in the tree; the lifecycle owns load and dispose
Adaptive banner size await AdSize.getAnchoredAdaptiveBannerAdSize(orientation, width) AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(width) — synchronous, so it fits inside LayoutBuilder (the Future form is kept too)
Native ad factory registration registerNativeAdFactory(engine, id, factory) Android registerNativeAdFactory(context, id, factory) — no FlutterEngine exists; iOS GMAKMobileAds.registerNativeAdFactory(id, factory:). The factory body is unchanged on both
Preloading Android + iOS Both, plus RewardedInterstitialAdPreloader, which upstream wires on Android only
Template colors dart:ui Color 32-bit ARGB int
Native ad height Comes from the platform view Reserved by you (template default or height:)
Rendering / transport PlatformView / MethodChannel Native view in the native hierarchy / dart:ffi
Ad Manager (GAM), mediation Supported Out of scope for 1.0
NativeAdOptions.shouldRequestMultipleImages, requestCustomMuteThisAd Supported Not exposed — no Next-Gen equivalent
Android SDK Legacy by default, USE_NEXT_GEN_SDK flag Next-Gen only

Full list with before/after code: doc/migration_from_flutter.md.

Android SDK choice

Flutter's plugin compiles from source inside your app, so a --dart-define can pick the SDK. A DartNative plugin ships as one prebuilt .aar, so the choice is made here, once: the GMA Next-Gen SDK, which Google documents as the default and labels play-services-ads as legacy. Consequences: minSdk 24 / compileSdk 35+; mediation is AdMob-only (out of scope for 1.0 anyway); nothing changes in your Dart code — the SDK's background-thread callbacks are marshalled back to the main thread inside the plugin.

Example

The example/ app exercises every format and preloading against Google's test ad units, including a custom native ad factory on both platforms — borrow from it freely.

Documentation

  • Migrating from Flutter
  • Design specification — architecture, decisions, and what was learned on device
  • Research notes — the DartNative and SDK internals this is built on (repository only)
  • AI agent skill: dart run skills@ get in an app that depends on this package installs google-mobile-ads-kit-usage, a compact reference for coding agents — setup, every format, and the pitfalls above.

Contributing

.claude/skills/dartnative-plugin/SKILL.md documents the plugin-authoring patterns this repository relies on (FFI, the dispatcher-slot callback contract, the JNI bridge, NativeElement). Read it before touching native code.

Credits & license

MIT — see LICENSE. The public API surface follows google_mobile_ads (Apache-2.0) to ease migration; no code is derived from it. The plugin ships its own native bridge — the Google Mobile Ads SDK 13.x on iOS, the GMA Next-Gen SDK on Android — over FFI, built on the DartNative framework's runtime.