
Declarative ad placements with a pure policy engine: frequency caps, warm‑up/cooldown, consent gating, adaptive Compose slots, AdMob provider, named suppression reasons and test tooling.
A Kotlin Multiplatform ads library for Android and iOS, with Compose Multiplatform ad slots and a Google AdMob provider.
You declare placements and the policies they follow. The library decides whether each placement may show, tells you why when it may not, and takes care of frequency caps, warm-up, consent gating and persistence.
RequiredSignalFalse(ads.consent_obtained) or DailyCapReached(12).AdBannerSlot reserves its height before the ad loads and the
banner is built at exactly that size, adaptive, 320x50 or a fixed height, so the layout does not
jump, including under mediation. AdNativeSlot renders through the SDK's own native ad view.ads-testing provides a fake provider, a controllable clock and an event recorder.| Artifact | Add to | Contents | Platforms |
|---|---|---|---|
ads-core |
commonMain |
Placements, policies, AdsSystem, events, the iOS provider |
Android, iOS, JVM |
ads-compose |
commonMain |
AdBannerSlot, AdNativeSlot, LocalAds, rememberAdDecision
|
Android, iOS |
ads-admob-android |
androidMain |
The AdMob provider and UMP consent for Android | Android |
ads-bridge-ios |
iosMain |
The Swift protocol your iOS app implements | iOS |
ads-testing |
commonTest |
FakeAdProvider, MutableAdClock, RecordingEventSink
|
Android, iOS, JVM |
kmp-ads is published to Maven Central. The AdMob SDK comes from Google's Maven repository, so keep both repositories:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}# gradle/libs.versions.toml
[versions]
kmpads = "1.0.0"
[libraries]
kmpads-core = { module = "io.github.shivathapaa.kmpads:ads-core", version.ref = "kmpads" }
kmpads-compose = { module = "io.github.shivathapaa.kmpads:ads-compose", version.ref = "kmpads" }
kmpads-admob-android = { module = "io.github.shivathapaa.kmpads:ads-admob-android", version.ref = "kmpads" }
kmpads-bridge-ios = { module = "io.github.shivathapaa.kmpads:ads-bridge-ios", version.ref = "kmpads" }
kmpads-testing = { module = "io.github.shivathapaa.kmpads:ads-testing", version.ref = "kmpads" }// the shared module's build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kmpads.core)
implementation(libs.kmpads.compose)
}
androidMain.dependencies {
implementation(libs.kmpads.admob.android)
}
iosMain.dependencies {
api(libs.kmpads.bridge.ios)
}
commonTest.dependencies {
implementation(libs.kmpads.testing)
}
}
}ads-bridge-ios is an api dependency because your iOS framework exports it. See
iOS setup.
Declare your placements:
object Placements {
val HomeBanner = AdPlacement(
id = AdPlacementId("home_banner"),
format = AdFormat.Banner,
policy = AdPolicy.InlineDefault,
)
val LevelComplete = AdPlacement(
id = AdPlacementId("level_complete"),
format = AdFormat.Interstitial,
policy = AdPolicy.FullScreenDefault,
)
val all = listOf(HomeBanner, LevelComplete)
}Create the ads system. On Android:
class App : Application() {
lateinit var ads: AdsSystem
lateinit var consent: AdConsentController
override fun onCreate() {
super.onCreate()
val adMob = AdMobAndroid.install(
application = this,
audience = AdAudienceConfig.GeneralAudience,
)
consent = adMob.consentController
ads = AdsSystem.create(
AdsConfig(
placements = Placements.all,
units = AdUnitRegistry.testOnly(),
provider = adMob.provider,
platform = AdPlatform.Android,
audience = AdAudienceConfig.GeneralAudience,
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
signals = mapOf(
AdSignalKey.ConsentObtained to consent.state.map { it.canRequestAds },
AdSignalKey.NetworkAvailable to flowOf(true),
AdSignalKey.UserIsPremium to flowOf(false),
),
hostDeclaresDebugBuild = BuildConfig.DEBUG,
)
).orNoOp()
}
}Gather consent once an Activity is resumed:
override fun onResume() {
super.onResume()
lifecycleScope.launch { (application as App).consent.gather() }
}Provide the system at the root of your UI and place ads:
CompositionLocalProvider(LocalAds provides ads) {
Scaffold(
bottomBar = { AdBannerSlot(placement = Placements.HomeBanner.id) },
) { padding ->
// Screen content
}
}Show a full-screen ad at a natural break:
scope.launch {
when (val outcome = ads.show(Placements.LevelComplete.id)) {
is AdShowOutcome.Completed -> outcome.reward?.let { grantReward(it) }
is AdShowOutcome.Suppressed -> log("Not shown: ${outcome.reason}")
AdShowOutcome.NotReady -> log("Nothing loaded yet")
is AdShowOutcome.Failed -> log("Failed: ${outcome.error}")
}
}On iOS the ads system is built the same way, with iosAdProvider(host) and
iosConsentController(host). The host is a small Swift adapter over Google Mobile Ads that you copy
from the sample. The integration guide covers both platforms step by step.
The sample app shows every format, the live decision for each placement and the event stream. It uses Google's demo ad units only.
./gradlew :sample:androidApp:installDebug # Android
open sample/iosApp/iosApp.xcodeproj # iOS, then run in XcodeThe iOS project resolves Google Mobile Ads through Swift Package Manager and builds the Kotlin framework itself.
| Requirement | Version |
|---|---|
| Kotlin | 2.4 or newer |
Compose Multiplatform (ads-compose) |
1.12 or newer |
| Android | minSdk 24, compileSdk 35 or newer |
| iOS | Google Mobile Ads 13.7 and User Messaging Platform 3.1 or newer, through Swift Package Manager |
Copyright 2026 Shiva Thapa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
A Kotlin Multiplatform ads library for Android and iOS, with Compose Multiplatform ad slots and a Google AdMob provider.
You declare placements and the policies they follow. The library decides whether each placement may show, tells you why when it may not, and takes care of frequency caps, warm-up, consent gating and persistence.
RequiredSignalFalse(ads.consent_obtained) or DailyCapReached(12).AdBannerSlot reserves its height before the ad loads and the
banner is built at exactly that size, adaptive, 320x50 or a fixed height, so the layout does not
jump, including under mediation. AdNativeSlot renders through the SDK's own native ad view.ads-testing provides a fake provider, a controllable clock and an event recorder.| Artifact | Add to | Contents | Platforms |
|---|---|---|---|
ads-core |
commonMain |
Placements, policies, AdsSystem, events, the iOS provider |
Android, iOS, JVM |
ads-compose |
commonMain |
AdBannerSlot, AdNativeSlot, LocalAds, rememberAdDecision
|
Android, iOS |
ads-admob-android |
androidMain |
The AdMob provider and UMP consent for Android | Android |
ads-bridge-ios |
iosMain |
The Swift protocol your iOS app implements | iOS |
ads-testing |
commonTest |
FakeAdProvider, MutableAdClock, RecordingEventSink
|
Android, iOS, JVM |
kmp-ads is published to Maven Central. The AdMob SDK comes from Google's Maven repository, so keep both repositories:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}# gradle/libs.versions.toml
[versions]
kmpads = "1.0.0"
[libraries]
kmpads-core = { module = "io.github.shivathapaa.kmpads:ads-core", version.ref = "kmpads" }
kmpads-compose = { module = "io.github.shivathapaa.kmpads:ads-compose", version.ref = "kmpads" }
kmpads-admob-android = { module = "io.github.shivathapaa.kmpads:ads-admob-android", version.ref = "kmpads" }
kmpads-bridge-ios = { module = "io.github.shivathapaa.kmpads:ads-bridge-ios", version.ref = "kmpads" }
kmpads-testing = { module = "io.github.shivathapaa.kmpads:ads-testing", version.ref = "kmpads" }// the shared module's build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kmpads.core)
implementation(libs.kmpads.compose)
}
androidMain.dependencies {
implementation(libs.kmpads.admob.android)
}
iosMain.dependencies {
api(libs.kmpads.bridge.ios)
}
commonTest.dependencies {
implementation(libs.kmpads.testing)
}
}
}ads-bridge-ios is an api dependency because your iOS framework exports it. See
iOS setup.
Declare your placements:
object Placements {
val HomeBanner = AdPlacement(
id = AdPlacementId("home_banner"),
format = AdFormat.Banner,
policy = AdPolicy.InlineDefault,
)
val LevelComplete = AdPlacement(
id = AdPlacementId("level_complete"),
format = AdFormat.Interstitial,
policy = AdPolicy.FullScreenDefault,
)
val all = listOf(HomeBanner, LevelComplete)
}Create the ads system. On Android:
class App : Application() {
lateinit var ads: AdsSystem
lateinit var consent: AdConsentController
override fun onCreate() {
super.onCreate()
val adMob = AdMobAndroid.install(
application = this,
audience = AdAudienceConfig.GeneralAudience,
)
consent = adMob.consentController
ads = AdsSystem.create(
AdsConfig(
placements = Placements.all,
units = AdUnitRegistry.testOnly(),
provider = adMob.provider,
platform = AdPlatform.Android,
audience = AdAudienceConfig.GeneralAudience,
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
signals = mapOf(
AdSignalKey.ConsentObtained to consent.state.map { it.canRequestAds },
AdSignalKey.NetworkAvailable to flowOf(true),
AdSignalKey.UserIsPremium to flowOf(false),
),
hostDeclaresDebugBuild = BuildConfig.DEBUG,
)
).orNoOp()
}
}Gather consent once an Activity is resumed:
override fun onResume() {
super.onResume()
lifecycleScope.launch { (application as App).consent.gather() }
}Provide the system at the root of your UI and place ads:
CompositionLocalProvider(LocalAds provides ads) {
Scaffold(
bottomBar = { AdBannerSlot(placement = Placements.HomeBanner.id) },
) { padding ->
// Screen content
}
}Show a full-screen ad at a natural break:
scope.launch {
when (val outcome = ads.show(Placements.LevelComplete.id)) {
is AdShowOutcome.Completed -> outcome.reward?.let { grantReward(it) }
is AdShowOutcome.Suppressed -> log("Not shown: ${outcome.reason}")
AdShowOutcome.NotReady -> log("Nothing loaded yet")
is AdShowOutcome.Failed -> log("Failed: ${outcome.error}")
}
}On iOS the ads system is built the same way, with iosAdProvider(host) and
iosConsentController(host). The host is a small Swift adapter over Google Mobile Ads that you copy
from the sample. The integration guide covers both platforms step by step.
The sample app shows every format, the live decision for each placement and the event stream. It uses Google's demo ad units only.
./gradlew :sample:androidApp:installDebug # Android
open sample/iosApp/iosApp.xcodeproj # iOS, then run in XcodeThe iOS project resolves Google Mobile Ads through Swift Package Manager and builds the Kotlin framework itself.
| Requirement | Version |
|---|---|
| Kotlin | 2.4 or newer |
Compose Multiplatform (ads-compose) |
1.12 or newer |
| Android | minSdk 24, compileSdk 35 or newer |
| iOS | Google Mobile Ads 13.7 and User Messaging Platform 3.1 or newer, through Swift Package Manager |
Copyright 2026 Shiva Thapa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.