2026-08-13 · 8 min read

How to Add AdMob to a Compose Multiplatform App on Android and iOS

Read on Medium ↗

AdMob CMP provides one shared Kotlin API over native Google Mobile Ads SDKs on Android and iOS.

A practical Kotlin Multiplatform guide to project setup, UMP consent, App Tracking Transparency, banner ads, interstitials, and safe test configuration.

To add Google AdMob to a Compose Multiplatform app, configure the native Android and iOS hosts, initialize one consent-aware manager from commonMain, and describe each ad placement with Android and iOS unit IDs. The shared Kotlin code can then render banners and control full-screen ads on both platforms.

This guide uses AdMob CMP, an open-source SDK that wraps Google Mobile Ads behind one Kotlin Multiplatform API. We will render a safe test banner and prepare an interstitial without building separate expect/actual ad managers.

The examples use AdMob CMP 2.0.0.

Before you begin, check the compatibility line for the version you plan to use. Version 2.0.0 is built with Kotlin 2.3.20 and Compose Multiplatform 1.11.1; it targets Android API 26 or newer and iOS 15 or newer.

What we are going to build

By the end, the shared module will own:

The native applications will still own the platform configuration Google requires: Android manifest metadata, iOS Swift packages, and Info.plist values.

1. Add AdMob CMP to commonMain

Add the facade dependency to the shared module:

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

Make sure the build resolves both Google’s and Maven Central’s repositories:

// settings.gradle.kts
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}

If the shared module runs Kotlin/Native tests such as iosSimulatorArm64Test, also apply the AdMob CMP Gradle plugin:

// shared/build.gradle.kts
plugins {
id("dev.avinya.ads.admob-cmp") version "2.0.0"
}

The plugin supplies the matching Google Mobile Ads and UMP frameworks to Kotlin/Native test executables. It does not replace Swift Package Manager in the production iOS app.

2. Configure the Android application

Google Mobile Ads requires an application ID in the runnable Android application module. Add the metadata inside the <application> element:

<!-- androidApp/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713" />
</application>
</manifest>

That is Google’s sample Android app ID. Replace it with the real app ID before release.

An app ID contains a tilde (~). An ad-unit ID contains a slash (/). Mixing them is a common setup mistake.

3. Configure the iOS application

AdMob CMP publishes Kotlin/Native bindings, but Google’s iOS binaries remain native dependencies of the host application.

In Xcode, choose File → Add Package Dependencies and add:

Select the GoogleMobileAds and GoogleUserMessagingPlatform products for the iOS app target.

Then add the sample application ID and the ATT usage description to Info.plist:

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3940256099942544~1458002511</string>

<key>NSUserTrackingUsageDescription</key>
<string>This identifier is used to deliver personalised ads to you.</string>

Before release, replace the sample app ID, add Google’s current SKAdNetworkItems, and review the App Store privacy disclosure. The full iOS setup guide keeps those native requirements in one place.

If a static Kotlin framework later fails with _OBJC_CLASS_$_JSContext, add this to the app target’s Other Linker Flags:

$(inherited) -framework JavaScriptCore

4. Initialize AdMob after UMP consent and iOS ATT

Call rememberAdManager() once near the root of the shared Compose application. Provide that same manager to the rest of the tree through LocalAdManager.

For iOS, the production order is UMP consent, then App Tracking Transparency, then Google Mobile Ads initialization. An initialization hook places the ATT request between consent and native SDK startup. On Android, the tracking controller is a no-op.

@Composable
fun App() {
val adManager = rememberAdManager()

LaunchedEffect(adManager) {
val config = AdConfig(
androidAppId = TestAdIds.ANDROID_APP_ID,
iosAppId = TestAdIds.IOS_APP_ID,
initializationHooks = listOf(
object : AdInitializationHook {
override suspend fun onPhase(
phase: AdInitializationPhase,
config: AdConfig,
) {
if (phase == AdInitializationPhase.BeforeMobileAdsInitialize) {
adManager.tracking.requestAuthorization()
}
}
}
),
)

adManager.gatherConsentAndInitialize(config)
}

val status by adManager.status.collectAsState()

if (status is AdManagerStatus.Ready) {
CompositionLocalProvider(LocalAdManager provides adManager) {
MainAdScreen(adManager)
}
} else {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
}

gatherConsentAndInitialize() requests a UMP update, shows the consent form when required, and initializes ads only when requests are allowed. The hook handles the separate iOS ATT step before native initialization.

Do not request ads merely because the app has entered composition. Gate ad-dependent UI on AdManagerStatus.Ready.

5. Render a banner from commonMain

Create one stable placement. A placement gives the logical location a finite ID and resolves the correct ad-unit ID for each platform.

@Composable
fun MainAdScreen(adManager: AdManager) {
val bannerPlacement = remember {
AdPlacement(
id = "home_banner",
format = AdFormat.Banner,
androidAdUnitId = TestAdIds.ANDROID_BANNER,
iosAdUnitId = TestAdIds.IOS_BANNER,
strictTestMode = true,
)
}

Column {
Text("My Compose Multiplatform app")

BannerAdView(
placement = bannerPlacement,
modifier = Modifier.fillMaxWidth(),
onEvent = { event ->
when (event) {
is AdEvent.Loaded -> println("Banner loaded")
is AdEvent.LoadFailed -> println("Banner failed: ${event.error}")
else -> Unit
}
},
)

InterstitialButton(adManager)
}
}

BannerAdView measures its Compose container and passes the width to the platform banner controller. That matters for adaptive banners on resizable layouts, including iPad split view and Slide Over.

Keep placement IDs static and finite. Do not create IDs such as "banner_${item.id}" for an unbounded list; the manager caches controllers by placement ID for its lifetime.

6. Load and show an interstitial

Full-screen formats use controllers rather than composables. Create the controller with remember, preload the ad before the natural break where it may be shown, and call show() from a UI-scoped coroutine.

@Composable
fun InterstitialButton(adManager: AdManager) {
val placement = remember {
AdPlacement(
id = "article_complete",
format = AdFormat.Interstitial,
androidAdUnitId = TestAdIds.ANDROID_INTERSTITIAL,
iosAdUnitId = TestAdIds.IOS_INTERSTITIAL,
strictTestMode = true,
)
}

val interstitial = remember(adManager) {
adManager.interstitial(placement)
}
val scope = rememberCoroutineScope()

LaunchedEffect(interstitial) {
interstitial.load()
}

Button(
onClick = {
scope.launch {
when (val result = interstitial.show()) {
is AdShowResult.Shown -> {
// The ad was shown and has now been dismissed.
}
is AdShowResult.NotReady -> {
// Nothing was ready. Continue without blocking the user.
}
is AdShowResult.Failed -> {
println("Show failed: ${result.error}")
}
}
}
},
) {
Text("Continue")
}
}

show() suspends until the ad is dismissed. Do not launch it in GlobalScope, and do not assume that returning from the call always means the ad was shown. Branch on AdShowResult.

Also avoid loading at the exact moment the user taps Continue. Preload earlier, then treat NotReady as a normal path that should not trap the user behind an ad request.

7. Use test ads without risking live traffic

The safest development setup uses Google’s official sample IDs through TestAdIds and sets strictTestMode = true on every test placement.

The two similarly named flags serve different purposes:

For QA against your own production ad units, register the physical device’s hashed test ID in GlobalRequestConfiguration.testDeviceIds instead. Emulators and simulators are automatically treated as test devices.

Never click live ads during development. A debug flag that only changes consent behavior is not protection against invalid traffic.

8. Add the other AdMob formats

The same shared manager exposes all six Android and iOS formats:

Rewarded and app-open formats have extra product rules, while native ads need stable logical slot keys and session ownership. Start with the dedicated format guides instead of copying a banner lifecycle into every format.

Production checklist

Before replacing the sample IDs, verify each of these deliberately:

  1. Use real AdMob app IDs in the Android manifest and iOS Info.plist.
  2. Use real Android and iOS ad-unit IDs in every production placement.
  3. Disable UMP debug geography and consent test devices.
  4. Set strictTestMode = false only in the production configuration.
  5. Keep UMP consent before ATT and ATT before the first iOS ad request.
  6. Show privacy settings only when privacyOptionsRequirementStatus is Required.
  7. Complete Google Play Data Safety and App Store privacy disclosures for the app’s actual configuration.
  8. Test on real Android and iOS devices, including denied consent, denied ATT, no-fill, offline, background, and foreground paths.

For iOS diagnostics, run:

./gradlew :shared:doctorIos
./gradlew :shared:iosSimulatorArm64Test

The first command reports configuration problems. The second proves that the consumer’s Kotlin/Native test executable can link the Google frameworks.

Common questions

Does Google AdMob have an official Kotlin Multiplatform SDK?

Google publishes native Mobile Ads SDKs for Android and iOS, not a shared Kotlin Multiplatform API. AdMob CMP binds those native SDKs behind a common Kotlin contract; the host applications still complete Google’s required native configuration.

Can I integrate AdMob with expect/actual instead?

Yes. A small expect/actual bridge can be appropriate for one simple banner. The maintenance cost grows when the app adds full-screen formats, consent, ATT, caching, native-ad ownership, shared events, or Kotlin/Native tests. At that point, the bridge is becoming an application-specific ad SDK.

Where should ad placements live?

Use stable, finite, product-level IDs such as home_banner or article_complete. Keep them in shared configuration or provide them through LocalAdPlacements. Do not generate a new placement ID for each feed row or navigation instance.

Does AdMob CMP support desktop and web ads?

No. The advertising implementation targets Android and iOS. A larger Compose Multiplatform application can keep desktop and web targets ad-free while sharing the rest of its UI and business logic.

Where to go next

The complete AdMob CMP quickstart is the shortest path from an empty project to a rendered test ad. The documentation also covers consent, App Tracking Transparency, native ads, caching, revenue events, mediation, and troubleshooting.

You can inspect the implementation, run the showcase app, report edge cases, or star the project on GitHub.

If you are interested in why I built the SDK instead of maintaining another pair of expect/actual ad managers, read the first article in this series: I Couldn’t Find a Clean Way to Add AdMob to Compose Multiplatform, So I Built One.

AdMob CMP is an independent open-source project. It is not affiliated with or endorsed by Google. AdMob and Google Mobile Ads are trademarks of Google LLC.

Originally published on Medium.