Skip to content

Quickstart

Five minutes from now, an Android or iOS app that renders a real banner ad inside a Compose Multiplatform screen, using Google’s public sample ad units. No AdMob account, no ad unit of your own, no App Store / Play Store ceremony. The integration is exactly the integration you will ship — only the test ids will be replaced with your own before release.

The five minutes assume you already have a working Kotlin Multiplatform project with a Compose Multiplatform screen in commonMain. If you do not, start from the KMP project wizard in your IDE; the only deviation from the default is adding the dependency line in the next step.

The quickstart deliberately uses Google’s sample ad units so no AdMob account is needed. The full installation story — version catalogs, the Gradle plugin, Kotlin/Native test linking — is on Installation.

shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("dev.avinya.ads:admob-cmp:1.1.0")
}
}
}

The Android GMA Next-Gen SDK and UMP arrive as transitive Maven dependencies, so no extra Android dependency is needed. iOS deliberately does not include the binaries — the app links them through Swift Package Manager. That sounds like extra work for a greenfield project, and it is the first time only: see iOS setup for the two package URLs and the three Info.plist keys. The architecture reference explains why the distribution works that way.

How do I initialize AdMob in Compose Multiplatform?

Section titled “How do I initialize AdMob in Compose Multiplatform?”

Initialize once at the root of your app, ideally as a side effect on first composition. The library gathers consent and starts Mobile Ads in one call:

@Composable
fun App() {
val adManager = rememberAdManager()
LaunchedEffect(Unit) {
adManager.gatherConsentAndInitialize(
AdConfig(
androidAppId = TestAdIds.ANDROID_APP_ID,
iosAppId = TestAdIds.IOS_APP_ID,
testMode = true,
)
)
}
val status by adManager.status.collectAsState()
if (status is AdManagerStatus.Ready) {
AdScreen()
}
}

Each piece is doing real work. rememberAdManager() returns a process-wide singleton — the same one AdMob.manager(context) returns on Android, so there is never a second manager in a process. gatherConsentAndInitialize runs the UMP consent flow, invokes any initialization hooks, then starts Mobile Ads. And status is the gate the rest of the UI is built on: AdManagerStatus is Idle, Initializing, ConsentRequired, Ready, Disabled(reason), or Failed(error, retryable).

A practical note on the gate: gating the entire ad screen on Ready is fine for the first integration, but the production app usually wants to preload placements in the background and only block the first show. The common shape is to keep showing app content while Initializing, and to flip the global UI gate the first time a show() button is pressed. The interstitial guide covers the preload-and-show pattern in detail.

Initialization order: UMP consent, then ATT, then initializeCLAUDE.md invariants #5, #11Read this diagram in words

The order on iOS matters. UMP consent, then App Tracking Transparency, then the first ad request — that is the only sequence that preserves the IDFA for personalised ads. App Tracking Transparency explains why requesting ads before ATT resolves permanently forfeits the IDFA for those requests.

val placement = remember {
AdPlacement(
id = "home_banner",
format = AdFormat.Banner,
androidAdUnitId = TestAdIds.ANDROID_BANNER,
iosAdUnitId = TestAdIds.IOS_BANNER,
strictTestMode = true,
)
}
BannerAdView(
placement = placement,
modifier = Modifier.fillMaxWidth(),
onEvent = { event ->
when (event) {
is AdEvent.Loaded -> println("banner loaded")
is AdEvent.LoadFailed -> println("banner failed: ${event.error}")
else -> Unit
}
},
)

BannerAdView measures its own container, supplies the width, loads, attaches, refreshes and disposes the platform banner view for you. Setting strictTestMode = true throws at construction if the placement points at a production ad unit, which is exactly what you want in a debug build — a test build can never request live ads. The remember around the placement matters because controllers are placement-keyed and cached for the manager’s lifetime, so ids must be static and finite.

The onEvent callback exposes the full AdEvent sealed hierarchy: Loaded, LoadFailed, Impression, Clicked, OpenedFullScreen, ClosedFullScreen and Paid. Most apps only log in development. Production code usually hoists ad events up to a single analytics collector — see Revenue and paid events for the paid-event shape and the cross-platform gotchas.

A short triage, each item the one thing that is usually wrong:

  • The manager is not AdManagerStatus.Ready yet. Gate the UI on status before placing any BannerAdView or calling load().
  • Consent has not been granted, so loads fail with AdErrorCode.CONSENT_REQUIRED. Run gatherConsentAndInitialize and check adManager.consent.canRequestAds.
  • The Android manifest is missing com.google.android.gms.ads.APPLICATION_ID. GMA crashes at startup if it is missing. See Android setup.
  • The iOS app has not added the two Swift Package Manager packages. See iOS setup.
  • The banner placement uses BannerRefreshPolicy.Manual and nothing called refresh(). Manual policy is no auto-load; you must call adManager.banner(placement).refresh() yourself.
  • The UMP consent form was dismissed without a choice, so canRequestAds is still false. The privacy-options form must be reopened and the user must accept before any request reaches the network.

When none of those explain it, the full symptom table is on Troubleshooting.

Five swaps. None of them changes the structure of the integration:

  1. Replace both sample app ids and every sample ad-unit id with your own.
  2. Set testMode = false on AdConfig. Leaving it on means the consent debug settings still apply.
  3. Keep strictTestMode = true in debug builds only.
  4. Complete the Play Data safety declaration in the Play Console.
  5. Add NSUserTrackingUsageDescription to the iOS Info.plist, or every iOS request will serve non-personalised ads at materially lower eCPM. See App Tracking Transparency.

The test ids are recognisable on sight: TestAdIds.ANDROID_BANNER is ca-app-pub-3940256099942544/9211549742, which is the same id Google uses in its own sample apps. The release-time checklist above is the simplest way to keep a debug build from ever being mistaken for a release build once you are past the quickstart.