What is AdMob CMP?
What is AdMob CMP?
Section titled “What is AdMob CMP?”AdMob CMP is a KMP AdMob library — a Kotlin Multiplatform wrapper over the
Google Mobile Ads SDKs that ships a single Kotlin API for the formats AdMob
publishers actually use, on both Android and iOS. The Android side binds the
GMA Next-Gen SDK (1.3.0) and the iOS side binds GMA 13.x through Kotlin/Native
cinterop, so the same rememberAdManager() call, the same AdPlacement, and
the same AdEvent stream work on both platforms. The published coordinate is
dev.avinya.ads:admob-cmp under the dev.avinya.ads group, at version
1.1.0. The Android app runs on API 26 or above and the iOS app targets iOS
15 or above.
The library keeps AdMob’s vocabulary — AdValue, response info, adaptive
banner sizes, UMP consent states, native asset names — but replaces the
listener-style SDK surface with suspend functions, StateFlow state, and a
single sealed AdEvent stream. You write the integration once and it runs
identically on both platforms, with platform-specific behaviour isolated to
thin Android and iOS adapters that the consumer never sees. Platform
specifics stay where they belong: the Android app’s AndroidManifest.xml
declares the AdMob app id, the iOS app’s Info.plist declares
GADApplicationIdentifier, and everything in between is shared code.
The split is deliberate. The library is published as two modules behind a
single coordinate: admob-cmp-core holds the controllers, state machines,
and platform adapters, while admob-cmp-compose holds the composable
surface. Most consumers depend on the umbrella dev.avinya.ads:admob-cmp
artifact and never notice the split. Apps that want the controller API
without Compose Multiplatform can depend on admob-cmp-core directly.
Which ad formats does it support?
Section titled “Which ad formats does it support?”Every format in the current AdMob catalogue, behind a single controller-per-format API:
| Format | Controller | Composable | Guide |
|---|---|---|---|
| Banner (incl. collapsible) | adManager.banner(placement) | BannerAdView | Banner ads |
| Interstitial | adManager.interstitial(placement) | — | Interstitial ads |
| Rewarded | adManager.rewarded(placement) | — | Rewarded ads |
| Rewarded interstitial | adManager.rewardedInterstitial(placement) | — | Rewarded ads |
| App-open | adManager.appOpen(placement) + AppOpenAdCoordinator | — | App-open ads |
| Native | adManager.nativeAd(placement) (a pool) | NativeAdView | Native ads |
The controllers are placement-keyed and cached for the manager’s lifetime,
so an app with three banner placements owns three BannerAdController
instances — not a fresh one per recomposition. NativeAdView is the one
exception in shape: it is a pool, because native ads are single-use.
What does it give you that the raw SDKs do not?
Section titled “What does it give you that the raw SDKs do not?”Four things the library owns so the app does not have to:
- A placement-keyed controller cache so the same
AdPlacementalways resolves to the same instance with the same load state. - A TTL’d FIFO ad cache with capped exponential retry, configurable per
placement through
AdCachePolicy,AdExpirationPolicyandAdRetryPolicy. - A consent gate that makes a pre-consent ad request impossible by
construction — every load checks
canRequestAdsand fails fast withAdErrorCode.CONSENT_REQUIREDwhen the user has not opted in. - One event stream instead of five callback interfaces, with
AdEventcoveringLoaded,LoadFailed,Impression,Clicked,Paid,RewardEarnedand the iOS-only video family.
The whole integration looks like this:
@Composablefun App() { val adManager = rememberAdManager()
LaunchedEffect(Unit) { adManager.gatherConsentAndInitialize( AdConfig( androidAppId = TestAdIds.ANDROID_APP_ID, iosAppId = TestAdIds.IOS_APP_ID, ) ) }
val placement = remember { AdPlacement( id = "main_interstitial", format = AdFormat.Interstitial, androidAdUnitId = TestAdIds.ANDROID_INTERSTITIAL, iosAdUnitId = TestAdIds.IOS_INTERSTITIAL, ) } val interstitial = remember(adManager) { adManager.interstitial(placement) } val scope = rememberCoroutineScope()
Button( onClick = { scope.launch { interstitial.load() interstitial.show() } } ) { Text("Show ad") }}Two caveats worth knowing on day one. show() suspends for the ad’s whole
on-screen lifetime, so it must run in a UI-scoped coroutine — never in
GlobalScope. And a second show() on the same controller while one is
still on screen returns AdShowResult.NotReady rather than queueing; await
the first result before calling again. Both rules are documented in detail
on the interstitial guide.
The cache and retry layer is the second thing people underestimate. A
naive integration against the raw SDKs opens a TCP connection, calls GMA’s
load, waits on a listener, parses the response, and only then exposes
state. Every app that ships a full-screen ad eventually writes that
caching, eviction, and retry layer themselves — usually wrong, and
usually twice (once per platform). The library writes it once in shared
code, with the policy surfaces the consumer needs (AdCachePolicy,
AdExpirationPolicy, AdRetryPolicy, AdTimeoutPolicy) and nothing
else. The caching, retry and timeouts guide
goes through every knob.
How does it compare with other Kotlin Multiplatform AdMob libraries?
Section titled “How does it compare with other Kotlin Multiplatform AdMob libraries?”A neutral look at capability, not at quality:
| Capability | AdMob CMP | Typical Kotlin Multiplatform AdMob wrapper |
|---|---|---|
| Banner, interstitial, rewarded | Yes | Yes |
| Rewarded interstitial | Yes | Varies |
| App-open ads | Yes | Usually not offered |
| Native ads | Yes, with a layout DSL and an ad pool | Usually not offered |
| UMP consent inside the init flow | Yes, with ConsentMode and the privacy options form | Usually a standalone consent call |
| iOS ATT ordering handled explicitly | Yes | Varies |
| Paid/revenue events and mediation response info | Yes | Varies |
| Kotlin/Native test linking on iOS | Solved by a published Gradle plugin | Usually unaddressed |
| iOS distribution model | Bindings-only cinterop; the app links GMA via SPM | Varies |
The closest alternative in this space is
LexiLabs-App/basic-ads. It
covers banner, interstitial and rewarded formats. If those three formats are
all a project needs and its API shape suits the codebase better, it is a
reasonable choice. AdMob CMP exists for projects that also need native ads,
app-open ads, consent wired into initialization, or Kotlin/Native tests that
link on iOS.
Which platforms and versions does it run on?
Section titled “Which platforms and versions does it run on?”| Format | Android — GMA Next-Gen, API 26+ | iOS — GMA 13.x, iOS 15+ | Notes |
|---|---|---|---|
| Banner | Supported | Supported | Anchored adaptive, inline adaptive, fixed and fluid; collapsible on both platforms |
| Interstitial | Supported | Supported | TTL'd FIFO cache, 1 hour |
| Rewarded | Supported | Supported | RewardEarned event |
| Rewarded interstitial | Supported | Supported | Same controller shape as Rewarded |
| App-open | Supported | Supported | AppOpenAdCoordinator, cooldowns, blocking; cache TTL 4 hours |
| Native | Supported | Supported | adLayout DSL and pooling on both; iOS renders through UIKit |
| Capability | Android | iOS | Notes |
|---|---|---|---|
| UMP consent and privacy-options form | Supported | Supported | canRequestAds gates every request on both platforms |
| App Tracking Transparency | Not applicable | Supported | Android has no ATT: adManager.tracking always reports AdTrackingAuthorization.NotApplicable, and the AD_ID manifest permission governs the advertising id there instead |
| Mediation adapters | Supported | Supported | No adapters are bundled; add the ones you need |
| Paid and revenue events | Supported | Supported | AdEvent.Paid carries AdValue and response info |
| Native video events | Not available | Supported — five events | iOS emits VideoStarted, VideoPlayed, VideoPaused, VideoEnded and VideoMuted through GADVideoControllerDelegate. The Android GMA Next-Gen SDK exposes no equivalent callback surface on NativeAd, so Android emits none. This is an upstream SDK gap, not an admob-cmp omission — do not rely on native video events for cross-platform logic. |
| immersiveMode, customClickGesture, publisherProvidedId, categoryExclusions, skipUninitializedAdapters | Supported | Silently ignored | Android-only request options |
| customTargeting, placementId | Supported | Supported | Mapped on both platforms; placementId maps to placementID on iOS |
Android is API 26 or above, with the GMA Next-Gen SDK and UMP arriving as
transitive Maven dependencies. iOS is 15.0 or above, with the GMA and UMP
frameworks linked by the app through Swift Package Manager. The full version
table — Kotlin, Compose Multiplatform, Android minSdk, iOS deployment
target, and the GMA / UMP versions each release was built against — is on
Compatibility.
What does AdMob CMP deliberately not do?
Section titled “What does AdMob CMP deliberately not do?”A short list, because the omissions are features:
- It ships zero mediation adapters by design. Adapters are platform binaries that must match the GMA SDK version and the publisher’s network contracts; bundling them would pin GMA versions for every consumer. Add them to your Android module and Xcode project directly. See Mediation.
- It never embeds Google’s binaries on iOS, only cinterop bindings. The app links the real GMA and UMP frameworks itself. See Architecture for the rationale.
- It is consumable from KMP and Gradle projects only, so a pure-Swift iOS app cannot adopt it without a Kotlin Multiplatform shim.
- It does not support ad networks other than AdMob directly. Mediation covers that.
Is AdMob CMP a consent management platform?
Section titled “Is AdMob CMP a consent management platform?”No. In ad-tech, CMP normally means Consent Management Platform — the vendor
that brokers GDPR / TCF consent. This library’s name predates that
association in the project’s own history and is retained for continuity with
the published Maven coordinate dev.avinya.ads:admob-cmp. It integrates
Google’s User Messaging Platform to gather GDPR / TCF consent, but it is
not itself a CMP vendor. The consent flow is documented on
UMP consent.