Skip to content

Mediation

Why does AdMob CMP ship zero mediation adapters?

Section titled “Why does AdMob CMP ship zero mediation adapters?”

Adapters are platform binaries that must match the GMA SDK version and the publisher’s network contracts. Bundling them inside a cross-platform wrapper would pin versions for everyone and break the moment two artifacts carry the same Objective-C class.

The property that makes this work on iOS specifically: because the library links nothing itself and ships bindings only, adapters resolve against the single copy of GMA in the app, so there is no duplicate-class problem. The full rationale is on Architecture.

A consequence worth being explicit about: the same is not true on Android if an adapter is published as part of the Android module that also depends on admob-cmp-core. The GMA Android SDK uses a different mechanism — adapters register against a single singleton at runtime — so the duplicate-class problem does not arise, and bundling an adapter at the consumer side is the right shape. The architecture reference has the full platform-by-platform model.

// Android — app or shared Android module
dependencies {
implementation("com.google.ads.mediation:facebook:…")
}

On iOS, add the adapter’s Swift Package or CocoaPod to the Xcode project, alongside the GoogleMobileAds package added during iOS setup.

A few operational notes. Waterfalls and bidding are configured in the AdMob UI as normal. Mediated fill flows through every existing API with no special casing. And the adapter version must be compatible with the GMA version in the app on each platform independently, which is the main source of “it works on Android” reports. The Play Data safety guide covers the disclosure side of enabling mediation.

The “works on one platform” failure mode has a specific shape. An adapter version that is compatible with GMA 13.x on iOS may not be compatible with GMA Next-Gen 1.3.0 on Android, and the incompatibility surfaces as a missing-class crash at adapter init time, not as a graceful “this adapter is not loaded”. The fix is to check the adapter’s compatibility matrix per platform before adding the dependency, and to keep the GMA version consistent across the project’s two platforms even though the library does not enforce that.

How do I set a network’s privacy flags before initialization?

Section titled “How do I set a network’s privacy flags before initialization?”

State the problem first: many networks require privacy flags to be set before SDK initialization, and there is no cross-platform place to do that unless the ad library provides one. AdInitializationHook is that place.

val metaConsentHook = object : AdInitializationHook {
override suspend fun onPhase(phase: AdInitializationPhase, config: AdConfig) {
when (phase) {
AdInitializationPhase.BeforeConsentRequest -> Unit
AdInitializationPhase.BeforeMobileAdsInitialize -> {
// Call into the adapter's platform API behind an expect/actual:
// Android: AdSettings.setDataProcessingOptions(arrayOf("LDU"))
// iOS: FBAdSettings.setDataProcessingOptions(["LDU"])
applyNetworkPrivacyFlags()
}
AdInitializationPhase.AfterMobileAdsInitialize -> Unit
}
}
}
AdConfig(
androidAppId = "ca-app-pub-…",
iosAppId = "ca-app-pub-…",
initializationHooks = listOf(metaConsentHook),
)

Three properties precisely. The hook body is the caller’s code, so adapter-specific calls belong in androidMain/iosMain behind an expect/actual and are invoked from the hook. The three phases are BeforeConsentRequest, BeforeMobileAdsInitialize and AfterMobileAdsInitialize. And hooks run exactly once per real native initialization attempt, inside a detached scope, so cancelling one initialize() caller can never skip or duplicate a hook.

The expect/actual pattern is the right shape for adapter privacy calls because the network SDKs are themselves platform-specific. The common side declares a suspending applyNetworkPrivacyFlags() function; androidMain calls AdSettings.setDataProcessingOptions(arrayOf("LDU")) (for Meta’s Limited Data Use flag) or the equivalent for the chosen network; iosMain does the same against the iOS class. The hook body stays the same on both platforms.

How do I verify an adapter is actually serving?

Section titled “How do I verify an adapter is actually serving?”
// After adManager.status is AdManagerStatus.Ready
adManager.diagnostics.adapterStatuses().forEach { status ->
println("${status.adapterName}: initialized=${status.initialized} latencyMs=${status.latencyMillis}")
}
scope.launch { adManager.diagnostics.openAdInspector() }

AdapterInitializationStatus carries adapterName, initialized, latencyMillis and description. openAdInspector() launches Google’s Ad Inspector overlay and works on test devices, and is the authoritative way to confirm an adapter serves. And adManager.diagnostics.sdkVersion() reports the underlying GMA version, which is the first thing to check against an adapter’s compatibility matrix.

A practical note on latencyMillis. It is the time the adapter took to initialise, not the time it took to win an auction. A high value here means the adapter is taking long to register itself with GMA, which is sometimes an indication of a misconfigured network. A low value does not mean the adapter is winning impressions; it only means it is ready when GMA needs it.

openAdInspector() is the right tool for “I want to see everything” — it overlays Google’s own inspector on the running app, with the live mediation chain for the current screen, the device’s ad requests in flight, and a way to fire test requests against specific ad units. It works only on test devices and never in release builds, because Google gates it behind a debug-only call path. The test safety guide covers which devices count as test devices.

How do I see which network won a given impression?

Section titled “How do I see which network won a given impression?”
LaunchedEffect(Unit) {
adManager.events.collect { event ->
if (event is AdEvent.Loaded) {
val winner = event.responseInfo?.loadedAdNetworkResponseInfo
println("won by ${winner?.adSourceName} (${winner?.adapterClassName}) in ${winner?.latencyMillis} ms")
}
}
}

AdResponseInfo.loadedAdNetworkResponseInfo identifies the winning adapter while adNetworkResponseInfos is the whole mediation chain including the networks that failed, each with its own error. The failure entries in the chain are the fastest way to diagnose a mediation waterfall that is not filling, and they appear in AdEvent.LoadFailed as well as on successful loads with partial fills.

AdEvent.Paid carries the mediated revenue for an impression, which is covered in detail on Revenue and paid events. The chain visible on a paid event is the same chain visible on a load event — the mediation network that won is the same network that paid.

A common debugging pattern: log the full adNetworkResponseInfos list on every Loaded event, and look for the error fields. A network that consistently appears in the chain with a non-null error is one that is being asked to bid and failing, which is different from a network that is not being asked. The former is a configuration issue; the latter is a waterfall issue. Both look the same to a publisher who only watches loadedAdNetworkResponseInfo.