AdManageKit is a comprehensive Android library designed to simplify the integration and management of Google AdMob ads, Google Play Billing, and User Messaging Platform (UMP) consent.
Latest Version 4.4.5 is a patch release. No API changed. It fixes a BannerAdView bug that renders a blank banner slot inside Jetpack Compose: swapping the shimmer placeholder for the loaded AdView raises an ordinary requestLayout(), which cannot cross Compose's AndroidView interop boundary once any ancestor already carries a pending layout flag — so Compose never re-measured the subtree and the freshly attached AdView was left at 0×0. The ad loaded, onAdLoaded/onAdImpression fired and the impression was logged and billed, but the slot stayed blank until a rotation or resize forced a full traversal. Both the AdMob and BannerWaterfall success paths now force the measure/layout pass themselves. Also picks up the Next-Gen GMA SDK 1.4.0, AGP 9.3.2 and Firebase BOM 34.18.0. See Release Notes v4.4.5.
Version 4.4.4 is a critical billing hotfix. No API changed. It repairs a regression introduced in 4.4.3, where the Play Billing connection was never started at all: connectToGooglePlayBilling() guarded on billingClient.isReady(), which reports true the instant a client is built, so startConnection(...) was skipped on every fresh client. Setup never ran, no product details or entitlement were ever fetched, and there was no failure callback and no error log to explain it. If you ship the billing module on 4.4.3, upgrade. Also fixes a long-standing bug where a timed-out setup reported itself as initialized — permanently suppressing the host app's if (!initBillingFinish) initBilling() retry — and adds DEBUG logging across the billing connection lifecycle. See Release Notes v4.4.4.
⚠️ 4.4.3 is superseded. Its billing regression makes purchases unusable; use 4.4.4 or newer.
Version 4.4.3 is a bug-fix and dependency release. No API changed. Two silent-failure bugs are fixed, both of which cost money: a rewarded load the manager had given up on could sabotage the load that replaced it — discarding an ad a newer load had just delivered, and clearing the "loading" flag out from under a request still in flight — and billing could stop acknowledging purchases for the rest of the process, which lets Play auto-refund a completed purchase on day 3. Acknowledgment also no longer requires the purchase to match a configured product id. Picks up the Next-Gen GMA SDK 1.3.1, Yandex Mobile Ads 8.3.0, and refreshed AndroidX/Firebase/Compose BOMs. See Release Notes v4.4.3.
Version 4.4.2 is a bug-fix release. No API changed, but several fixes are user-visible and two affect revenue: rewarded ads could crash the app (every show-path callback, including onRewardEarned, was delivered on a background thread), a completed purchase could fail to disable ads until the next launch, blank gaps were left where banner/native slots should have collapsed for premium users and after failed loads (in both XML and Compose), and app open ads could appear over screens excluded via disableAppOpenAdsTemporarily(). Also fixes a BannerAdView leak that kept requesting ads after detach. Two deliberate behavior changes: an account-hold subscription no longer disables ads, and premium users no longer reserve ad space in Compose. See Release Notes v4.4.2.
Version 4.4.1 is a patch release that updates the Google Mobile Ads Next-Gen SDK to 1.3.0 (from 1.2.1) and repairs three documentation-delivery channels that had silently gone stale: the API docs build had failed on every release since the Dokka 2.x upgrade, leaving the published KDoc without any 4.x types; MCP server npm publishing had been broken since January 2026; and the MCP tool schemas hardcoded which versions and classes existed, so they rejected 4.4.0 as an unknown version. No AdManageKit API changed. See Release Notes v4.4.1.
Version 4.4.0 is a minor release focused on the billing module. Subscription offers can now be purchased individually — subscribe(activity, offer) buys exactly the plan the user tapped, where previously the library always resolved the token itself and could charge for whichever offer Play listed last. It also adds offer lookup by id/base plan/tag, cross-cadence price normalization (BillingPeriod, getSavingsPercent, getFormattedPricePerMonth), trial-eligibility checks, Play Billing 9 one-time product offers (discounts, rentals, pre-orders, limited quantity), client-side account hold detection, Play payment-recovery messaging, and product-details diagnostics. Purely additive, with one behavior change: an on-hold subscription now reports ON_HOLD and is no longer isSubscriptionActive(). See Release Notes v4.4.0.
Version 4.3.5 is a patch release that makes appOpenAdFreshnessThreshold (default 4 hours) actually enforced on every app open show path. Previously only ON_DEMAND consulted it — HYBRID, ONLY_CACHE, and the multi-provider waterfall checked only whether an ad object existed and could show a day-old cached ad. Stale ads are now discarded and replaced instead of shown. It also settles the appOpenAdTimeout default at 10 seconds (the property said 4s while resetToDefaults() said 10s — 4s was too short for the fetch-with-dialog paths). No API changes. See Release Notes v4.3.5.
Version 4.3.4 is a patch release that restores the pre-4.2.0 default adaptive banner height: ADAPTIVE again requests the standard anchored adaptive size (~50-90dp) instead of the taller large anchored adaptive format the Next-Gen migration had switched to. The taller format is now opt-in via BannerAdSize.ADAPTIVE_LARGE (app:bannerAdSize="adaptive_large" in XML). See Release Notes v4.3.4.
Version 4.3.3 is a patch release that improves native ad media quality: every native request now sends a media-aspect-ratio hint matched to each template's MediaView slot shape and carries global VideoOptions (start-muted by default). New AdManageKitConfig fields and a per-view NativeTemplateView.setMediaAspect(...) override expose the behavior; all defaults preserve prior request behavior. See Release Notes v4.3.3.
Version 4.3.2 is a patch release that fixes banner shimmer sizing — the placeholder now reserves the real adaptive-banner height from the first frame and in the layout preview instead of collapsing to ~50dp — and stops the Compose native ad from clipping its call-to-action button. Also bumps the Android Gradle Plugin to 9.3.0. See Release Notes v4.3.2.
Version 4.3.1 is a patch release fixing crashes from Next-Gen SDK callbacks delivered on background threads — interstitial onNextAction, banner/native waterfall chains, and NativeTemplateView.onFailedToLoad are now marshalled to the main thread — plus a load-site guard for app open ads. See Release Notes v4.3.1.
Version 4.3.0 adds all standard AdMob banner sizes (BannerAdSize), fully custom native ad templates on NativeTemplateView, and a redesigned size-adaptive banner shimmer with night-mode support. See What's New in 4.3.0.
Since 4.2.0 the library runs on the Google Mobile Ads Next-Gen SDK (ads-mobile-sdk, stable) and Google Play Billing Library 9.1.0 — see Next-Gen GMA SDK below and Migrating to 4.2.0 if you're upgrading from 3.x/4.1.
As of v4.2.0, AdManageKit runs on Google's Next-Gen Google Mobile Ads SDK (com.google.android.libraries.ads.mobile.sdk, stable 1.4.0 as of 4.4.5) instead of the legacy com.google.android.gms:play-services-ads. This isn't a branch or an opt-in — it's the only version of AdManageKit going forward.
The legacy Google Mobile Ads SDK is in maintenance mode; new AdMob features (Ad Inspector improvements, mediation updates, in-app price-increase messaging, etc.) land on the Next-Gen SDK first, and Google's own guidance is to move new integrations there. AdManageKit had a separate nextgen branch exploring this since it was in beta and diverging from main's waterfall/Yandex/billing work — now that the SDK is stable, that split is gone.
- Threading: Next-Gen SDK callbacks fire on a background thread (the legacy SDK guaranteed main thread). Every ad manager, provider, and view in this library already wraps its callback bodies in
Handler(Looper.getMainLooper()).post {}— this is transparent to you as a consumer. - Initialization:
MobileAds.initialize()must be called explicitly once, before any ad request — the legacy SDK's silent lazy-init on first use no longer exists. AdManageKit doesn't call this for you (it doesn't own your app's consent flow); see the sample app'sMyApplication.ktfor the recommended pattern. - Callback types: Where AdManageKit exposes ad-network error/value types through
AdKitError,AdKitLoadError, andAdKitValue(used byAdManagerCallback,AdLoadCallback,AdCallback, and friends), those aliases now resolve to Next-Gen SDK types instead of legacy ones. Your callback implementations (onFailedToLoad(error),onPaidEvent(value), etc.) don't need to change — only code that reads legacy-only members of those objects does. See Migrating to 4.2.0.
A billing-focused minor release. Everything is additive — no existing method signature changed — with one deliberate behavior change around account hold.
AppPurchase could already describe subscription offers (getOffers, getTrialOffer, getBaseOffer since 3.5.7), but it could not act on them. subscribe(activity, subsId) always resolved the offer token itself — the configured trialId, and failing that whichever offer Play listed last. On a product with three plans, a user tapping "Yearly" could be charged for monthly, purely because of list order.
val offers = AppPurchase.getInstance().getOffers("premium_sub")
// …render them, then buy exactly what the user chose:
AppPurchase.getInstance().subscribe(activity, offers[selectedIndex])The same applies to one-time products (purchase(activity, offer)) and upgrades (updateSubscription(activity, newSubsId, offerToken, oldToken, mode)). The original no-token overloads are unchanged.
billing.getIntroOffer("premium_sub") // first with an intro price
billing.getOfferByBasePlanId("premium_sub", "yearly")
billing.getOfferByTag("premium_sub", "popular") // Play Console offer tag
billing.getBestValueOffer("premium_sub") // lowest cost per month
billing.getCheapestFirstCycleOffer("premium_sub") // cheapest way inOffer tags are the useful one here: tag offers in the Play Console and select them by tag, and a paywall can be re-targeted without shipping an app update.
Play returns billing periods as raw strings ("P1M", "P1Y"). The new BillingPeriod parses them and normalizes prices so plans of different cadences compare fairly:
val period = BillingPeriod.parse("P1Y")!!
period.unit // Unit.YEAR
period.count // 1 → getQuantityString(R.plurals.years, count, count)
period.totalMonths // 12.0
billing.getSavingsPercent("premium_monthly", "premium_yearly") // 50 → "Save 50%"
billing.getFormattedPricePerMonth("premium_yearly") // "$5.00"OfferInfo also gained firstCyclePrice (what the user pays today — "Free", "$1.99" or "$9.99"), pricePerMonthMicros, introDiscountPercent, trialDays, introTotalDays, isBaseOffer and hasTag(...).
Google Play filters offers per account — a user who already used a trial simply doesn't receive that offer. So offer presence is the eligibility signal, now named explicitly:
button.text = if (billing.isEligibleForFreeTrial("premium_sub")) "Start free trial" else "Subscribe"This stops a paywall promising a trial the user cannot claim and then having Play charge them immediately.
- One-time product offers — a single INAPP product can carry several offers (a full price plus a launch discount, a rental, a pre-order, a limited-quantity drop). New
OneTimeOfferInfo+getOneTimeOffers()/getBestOneTimeOffer(); the legacygetOneTimePurchaseOfferDetails()exposes only one of them - Payment recovery —
showInAppMessages(activity, listener)shows Play's fix-your-payment flow for declined subscriptions - Product-details diagnostics —
setProductDetailsListener(...)andgetUnfetchedProducts()report which ids Play declined and why, so an empty paywall is no longer silent - Installment plans —
OfferInfo.isInstallmentPlanand commitment payment counts - Fraud prevention & EU disclosure —
setObfuscatedAccountId(),setObfuscatedProfileId(),setOfferPersonalized(), applied to every flow the library launches
When Play cannot charge a subscriber, the subscription enters account hold. This previously required server-side verification; Play Billing 9 reports it client-side, so PurchaseResult.isSuspended() is now populated and getSubscriptionState() returns ON_HOLD.
isSubscriptionActive()therefore returnsfalseduring account hold. That matches Google's requirement — the user's payment was declined and they must not keep premium access. If your app deliberately keeps serving on-hold users, checkisSuspended()explicitly.
if (billing.hasSubscriptionOnHold()) {
billing.showInAppMessages(activity, object : InAppMessageListener {
override fun onSubscriptionRecovered(purchaseToken: String) { refreshPremiumUi() }
override fun onNoActionNeeded() { }
})
}See Release Notes v4.4.0 and the Subscription Offers wiki page for full details.
BannerAdView and BannerAdCompose now support every standard AdMob banner size via the new BannerAdSize enum — ADAPTIVE (default, unchanged behavior), BANNER (320x50), LARGE_BANNER (320x100), MEDIUM_RECTANGLE (300x250), FULL_BANNER (468x60), LEADERBOARD (728x90). Select programmatically (loadBanner(activity, adUnitId, BannerAdSize.MEDIUM_RECTANGLE)), in XML (app:bannerAdSize="medium_rectangle"), or in Compose (adSize = BannerAdSize.MEDIUM_RECTANGLE). The size carries through retries, auto-refresh, and the multi-provider waterfall. See the Banner Ad Guide.
If none of the 38 built-in NativeTemplateView presets fit your design, supply your own layout: setCustomTemplate(layoutResId, shimmerResId, sizeHint) programmatically, app:customAdLayout/app:customAdShimmerLayout in XML, or customLayoutResId on NativeTemplateCompose. The layout's root must be a Next-Gen SDK NativeAdView reusing the standard asset ids. See the NativeTemplateView guide.
The banner loading placeholder now adapts to the requested size (a 300x250 request shows a proper media-block placeholder, not a lone 50dp row), reserves the exact ad size on every path including waterfalls (no layout jump), centers to match the loaded ad, and follows the system dark theme instead of rendering a hardcoded white card.
AppOpenManager no longer crashes if constructed before MobileAds.initialize() completes: every load path is guarded, callback-driven flows (splash screens) wait for the SDK within their existing timeout and always receive a terminal callback, and the new isMobileAdsReady() confirms the SDK can accept requests. See the App Open Ads guide.
Compatibility: source-compatible;
ADAPTIVEkeeps pre-4.3.0 behavior everywhere. Recompile against 4.3.0 rather than hot-swapping the AAR (Compose composables gained default parameters).
See Release Notes v4.3.0 for full details.
com.google.android.gms:play-services-ads is replaced by the stable Next-Gen SDK (ads-mobile-sdk:1.2.1) across every ad manager, provider, native view, and the Compose module. This is the change described in Next-Gen GMA SDK above — read that section first if you're upgrading.
Upgraded from 8.3.0. No code changes were needed in AppPurchase — the wrapper was already on v8+ patterns. Source- and behavior-compatible.
Required transitively by androidx-core-ktx 1.19.0+. Bump your app's compileSdk to 37+.
- Native ad
MediaViewrendering blank inNativeTemplateViewand the programmatic native provider (a leftover legacy-SDK pattern was interfering with the Next-Gen SDK's automatic media rendering) NativeAdView.mediaViewreturningnullfor dynamically-inflated templates (the getter's auto-discovery needs the view to be window-attached; affected views now usefindViewById()directly)- A Native Validator false-positive risk on deeply-nested templates like
CARD_MODERN, caused by registering the ad before the view had been through a layout pass
Compatibility: not fully source-compatible — see Migrating to 4.2.0. Most consumers (anyone who only calls methods on AdManageKit's own callback types, without custom native XML layouts) are unaffected.
See Release Notes v4.2.0 for full details.
For the 3.6.0 release (Yandex template matching, waterfall-aware programmatic loader), see Release Notes v3.6.0.
For previous versions, see the Changelog or individual release notes.
| NativeBannerSmall Ad | Interstitial Ad | App Open Ad | UMP Consent Form |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Watch a short demo of AdManageKit in action:
Step 1: Add JitPack to your root build.gradle:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}Step 2: Add dependencies to your app's build.gradle:
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit:v4.4.5'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-billing:v4.4.5'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-core:v4.4.5'
// For Jetpack Compose support
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-compose:v4.4.5'
// For Yandex Ads multi-provider support
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-yandex:v4.4.5'Step 3: Ensure your app's compileSdk is 37 or higher (required transitively as of 4.2.0).
Step 4: Sync your project with Gradle.
- 38 Template Styles: card_modern, material3, app_store, social_feed, gradient_card, pill_banner, medium_horizontal, flexible, icon_left, top_icon_media, spotlight, plus the video_* and flat_* families
- XML & Programmatic: Set templates via
app:adTemplateorsetTemplate() - Material 3 Theming: Automatic dark/light mode support
- AdChoices Control: Configure placement position (v2.9.0+)
- Video-Ready: All templates support video ads (120dp+ MediaView)
- View Documentation
- ON_DEMAND: Fetch fresh ads with loading dialog
- ONLY_CACHE: Instant display from cache
- HYBRID: Cache-first with fallback fetch (recommended)
- View Documentation
- BannerAdCompose, NativeAdCompose, InterstitialAdCompose
- Programmatic native ads without predefined layouts
- ConditionalAd, CacheWarmingEffect utilities
- Banner Ads: All standard sizes (adaptive, 320x50, 320x100, 300x250, 468x60, 728x90), auto-refresh, collapsible banners, smart retry
- Native Ads: Small, Medium, Large formats with caching
- Interstitial Ads: Time/count-based triggers, dialog support
- App Open Ads: Lifecycle-aware with activity exclusion
- AdManageKitConfig: Single configuration point
- Environment-specific settings (debug vs production)
- Runtime configuration changes
- Screen-aware caching prevents collisions
- Smart preloading with usage patterns
- LRU cache with configurable expiration
- Smart retry with exponential backoff
- Circuit breaker for failing ad units
- Memory leak prevention with WeakReference
- Subscriptions & One-Time Products: Categories (consumable, feature unlock, remove ads, lifetime premium) with automatic acknowledgment
- Offers: Enumerate every trial / introductory / base offer and purchase a specific one (v4.4.0+)
- Price Comparison: ISO-8601 period parsing, per-month normalization, "Save 40%" badges (v4.4.0+)
- Trial Eligibility: Ask whether this account can still claim a trial before promising one (v4.4.0+)
- Payment Recovery: Client-side account-hold detection plus Play's in-app recovery flow (v4.4.0+)
- In-App Purchase Guide
- UMP consent management (GDPR/CCPA)
- Automatic ad hiding for purchased users
- Obfuscated account/profile identifiers and EU personalized-pricing disclosure (v4.4.0+)
- Multiple Ad Networks: Load ads from AdMob, Yandex, and more with automatic fallback
- Zero Code Changes: Configure provider chains once; all existing API calls use waterfall automatically
- Per-Ad-Type Chains: Configure different provider orders for each ad format
- Region-Based: Prioritize providers by user locale (e.g., Yandex first for Russia)
- View Waterfall Documentation
- Yandex Integration Guide
- Core Module: Shared interfaces and configuration
- Compose Module: Jetpack Compose integration
- Billing Module: Google Play Billing Library v9
- Yandex Module: Yandex Ads SDK provider
Configure AdManageKit in your Application class:
class MyApp : Application() {
private lateinit var appOpenManager: AppOpenManager
override fun onCreate() {
super.onCreate()
// Configure AdManageKit
AdManageKitConfig.apply {
debugMode = BuildConfig.DEBUG
enableSmartPreloading = true
autoRetryFailedAds = true
// Ad Loading Strategies (v2.6.0+)
interstitialLoadingStrategy = AdLoadingStrategy.HYBRID
appOpenLoadingStrategy = AdLoadingStrategy.HYBRID
nativeLoadingStrategy = AdLoadingStrategy.HYBRID
// Auto-reload ads after showing (v2.7.0+)
interstitialAutoReload = true // default: true
appOpenAutoReload = true // default: true
rewardedAutoReload = true // default: true
}
// Set up billing
BillingConfig.setPurchaseProvider(BillingPurchaseProvider())
// Initialize app open ads
appOpenManager = AppOpenManager(this, "your-app-open-ad-unit-id")
}
}Add Yandex (or other providers) as fallback ad networks with zero changes to your existing ad loading code:
// Add Yandex module
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-yandex:v3.6.0'// In Application.onCreate(), after AdManageKitConfig setup:
YandexProviderRegistration.initialize(this)
val admob = AdMobProviderRegistration.create()
val yandex = YandexProviderRegistration.create()
// Map your AdMob ad unit IDs to Yandex equivalents
AdUnitMapping.register("ca-app-pub-xxx/your-interstitial", mapOf("yandex" to "R-M-XXXXXX-Y"))
AdUnitMapping.register("ca-app-pub-xxx/your-native", mapOf("yandex" to "R-M-XXXXXX-Y"))
// Set provider chains (order = priority)
AdProviderConfig.setInterstitialChain(listOf(admob.interstitialProvider, yandex.interstitialProvider))
AdProviderConfig.setNativeChain(listOf(admob.nativeProvider, yandex.nativeProvider))
// ... same for banner, app open, rewardedSee Multi-Provider Waterfall and Yandex Integration for the full guide.
<com.i2hammad.admanagekit.admob.NativeTemplateView
android:id="@+id/nativeTemplateView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:adTemplate="material3" />| Template | Best For |
|---|---|
card_modern |
General use |
material3 |
M3 apps |
minimal |
Content-focused |
compact_horizontal |
Lists |
list_item |
RecyclerView items |
magazine |
News/blog apps |
app_store |
App promotion (v2.9.0+) |
social_feed |
Feed integration (v2.9.0+) |
gradient_card |
Premium feel (v2.9.0+) |
pill_banner |
Inline placement (v2.9.0+) |
medium_horizontal |
55/45 media-content split (v3.0.0+) |
spotlight |
High visibility (v2.9.0+) |
media_content_split |
Balanced display (v2.9.0+) |
flexible |
Adaptive layout (v3.3.2+) |
icon_left |
Icon on left, GridView optimized (v3.3.2+) |
top_icon_media |
Icon at top, MediaView center (v3.3.2+) |
video_small/medium/large |
Video content |
video_square/vertical/fullscreen |
Social feeds |
// Load with default template
nativeTemplateView.loadNativeAd(activity, "ca-app-pub-xxx/yyy")
// Change template
nativeTemplateView.setTemplate(NativeAdTemplate.MAGAZINE)
nativeTemplateView.loadNativeAd(activity, "ca-app-pub-xxx/yyy")
// With callback
nativeTemplateView.loadNativeAd(activity, adUnitId, object : AdLoadCallback() {
override fun onAdLoaded() { /* success */ }
override fun onFailedToLoad(error: AdError?) { /* error */ }
})
// With strategy override
nativeTemplateView.loadNativeAd(activity, adUnitId, callback, AdLoadingStrategy.ONLY_CACHE)<com.i2hammad.admanagekit.admob.BannerAdView
android:id="@+id/bannerAdView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:bannerAdSize="adaptive" />bannerAdView.loadBanner(this, "ca-app-pub-xxx/yyy")
// Fixed size (BANNER, LARGE_BANNER, MEDIUM_RECTANGLE, FULL_BANNER, LEADERBOARD)
bannerAdView.loadBanner(this, "ca-app-pub-xxx/yyy", BannerAdSize.MEDIUM_RECTANGLE)
// Collapsible banner (adaptive size only)
bannerAdView.loadCollapsibleBanner(this, "ca-app-pub-xxx/yyy", true)All standard AdMob banner sizes are supported via BannerAdSize (v4.3.0); the
default ADAPTIVE is the Google-recommended full-width anchored adaptive banner
(~50-90dp), and ADAPTIVE_LARGE (v4.3.4) opts into the taller Next-Gen large
anchored adaptive format.
See Banner Ad Guide for the full size table.
<com.i2hammad.admanagekit.admob.NativeBannerSmall
android:id="@+id/nativeBannerSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content" />nativeBannerSmall.loadNativeBannerAd(this, "ca-app-pub-xxx/yyy")
// With caching
nativeBannerSmall.loadNativeBannerAd(activity, adUnitId, useCachedAd = true)// Load
AdManager.getInstance().loadInterstitialAd(this, "ca-app-pub-xxx/yyy")
// Show immediately
AdManager.getInstance().forceShowInterstitial(this, object : AdManagerCallback() {
override fun onNextAction() { navigateNext() }
})
// Show with dialog
AdManager.getInstance().forceShowInterstitialWithDialog(this, callback)
// Time-based (every 15 seconds)
AdManager.getInstance().showInterstitialAdByTime(this, callback)
// Count-based
AdManager.getInstance().showInterstitialAdByCount(this, callback, maxDisplayCount = 3)Control whether interstitial ads automatically reload after being shown:
// Global config (applies to all AdManager methods)
AdManageKitConfig.interstitialAutoReload = false // Disable auto-reload
// Per-call override via InterstitialAdBuilder
InterstitialAdBuilder.with(activity)
.adUnit(adUnitId)
.autoReload(true) // Override global setting for this call
.show { navigateNext() }
// Per-call override via AdManager
AdManager.getInstance().showInterstitialIfReady(activity, callback, reloadAd = false)Priority: InterstitialAdBuilder.autoReload() > AdManageKitConfig.interstitialAutoReload
// Initialize once (e.g., in Application.onCreate())
RewardedAdManager.initialize(context, "ca-app-pub-xxx/yyy")
// Show when ready
if (RewardedAdManager.isAdLoaded()) {
RewardedAdManager.showAd(activity, object : RewardedAdManager.RewardedAdCallback {
override fun onRewardEarned(rewardType: String, rewardAmount: Int) {
grantReward(rewardType, rewardAmount)
}
override fun onAdDismissed() {
continueGameFlow()
}
})
}
// Load with timeout (splash screens)
RewardedAdManager.loadRewardedAdWithTimeout(context, 5000, callback)
// Preload during natural pauses
RewardedAdManager.preload(context)
// Control auto-reload
AdManageKitConfig.rewardedAutoReload = false // Disable globally
RewardedAdManager.showAd(activity, callback, autoReload = false) // Per-call override// In Application class
appOpenManager = AppOpenManager(this, "ca-app-pub-xxx/yyy")
// Exclude activities
appOpenManager.disableAppOpenWithActivity(MainActivity::class.java)
// Force show
appOpenManager.forceShowAdIfAvailable(activity, callback)
// Skip next ad
appOpenManager.skipNextAd()For apps with one activity and multiple fragments:
// Set current screen when navigating
navController.addOnDestinationChangedListener { _, destination, _ ->
appOpenManager.setCurrentScreenTag(destination.label?.toString())
}
// Exclude specific screens
appOpenManager.excludeScreenTags("Payment", "Onboarding", "Checkout")
// Or use fragment tag provider
appOpenManager.setFragmentTagProvider {
supportFragmentManager.fragments.lastOrNull()?.tag
}
appOpenManager.excludeFragmentTags("PaymentFragment", "OnboardingFragment")
// Temporarily disable during critical flows
appOpenManager.disableAppOpenAdsTemporarily()
// ... perform operation ...
appOpenManager.enableAppOpenAds()@Composable
fun MyScreen() {
// Banner
BannerAdCompose(adUnitId = "ca-app-pub-xxx/yyy")
// NativeTemplateView with any template (v2.6.0+)
NativeTemplateCompose(
adUnitId = "ca-app-pub-xxx/yyy",
template = NativeAdTemplate.MATERIAL3,
loadingStrategy = AdLoadingStrategy.HYBRID
)
// Native with loading strategy (ON_DEMAND or HYBRID only)
NativeBannerMediumCompose(
adUnitId = "ca-app-pub-xxx/yyy",
loadingStrategy = AdLoadingStrategy.HYBRID
)
// Interstitial
val showInterstitial = rememberInterstitialAd(
adUnitId = "ca-app-pub-xxx/yyy",
preloadAd = true
)
Button(onClick = { showInterstitial() }) {
Text("Show Ad")
}
// Conditional (hides for purchased users)
ConditionalAd {
ProgrammaticNativeBannerMediumCompose(adUnitId = "ca-app-pub-xxx/yyy")
}
}AdsConsentManager.getInstance(this).requestUMP(
activity = this,
isDebug = true,
testDeviceId = "TEST_DEVICE_ID",
resetConsent = false,
listener = object : UMPResultListener {
override fun onCheckUMPSuccess(isConsentGiven: Boolean) {
if (isConsentGiven) {
// Initialize and load ads here
AdManager.getInstance().loadInterstitialAd(activity, adUnitId)
}
}
}
)// Define products with categories
val products = listOf(
PurchaseItem("coins_100", TYPE_IAP.PURCHASE, PurchaseCategory.CONSUMABLE),
PurchaseItem("remove_ads", TYPE_IAP.PURCHASE, PurchaseCategory.REMOVE_ADS),
PurchaseItem("lifetime", TYPE_IAP.PURCHASE, PurchaseCategory.LIFETIME_PREMIUM),
PurchaseItem("premium_monthly", "free_trial", TYPE_IAP.SUBSCRIPTION)
)
// Initialize
AppPurchase.getInstance().initBilling(application, products)
// Purchase
AppPurchase.getInstance().purchase(activity, "remove_ads")
// Subscribe
AppPurchase.getInstance().subscribe(activity, "premium_monthly")
// Check status
if (AppPurchase.getInstance().isPurchased()) {
// User has premium (subscription, lifetime, or remove_ads)
}
// Track purchases and handle consumables
AppPurchase.getInstance().setPurchaseHistoryListener(object : PurchaseHistoryListener {
override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
if (productId == "coins_100") {
addCoins(100 * purchase.quantity)
AppPurchase.getInstance().consumePurchase(productId) // Manual consume
}
}
override fun onPurchaseConsumed(productId: String, purchase: PurchaseResult) { }
})A single subscription product usually carries several offers — a base plan, a free trial, an introductory discount. Play returns only the ones this account is eligible for.
val billing = AppPurchase.getInstance()
// Enumerate every offer, then buy exactly the one the user tapped.
// (subscribe(activity, subsId) picks for you, and may not pick what they chose.)
val offers = billing.getOffers("premium_sub")
billing.subscribe(activity, offers[selectedIndex])
// Or find a specific one
billing.getIntroOffer("premium_sub") // first with an intro price
billing.getOfferByBasePlanId("premium_sub", "yearly")
billing.getOfferByTag("premium_sub", "popular") // Play Console offer tag
billing.getBestValueOffer("premium_sub") // lowest cost per monthRender an offer without hand-parsing ProductDetails or ISO-8601 periods:
val offer = billing.getBaseOffer("premium_yearly") ?: return
priceLabel.text = offer.basePrice // "$59.99"
cycleLabel.text = BillingPeriod.formatOf(offer.billingPeriod) // "1 year"
perMonthLabel.text = AppPurchase.formatPrice(offer.pricePerMonthMicros, offer.currencyCode)
todayLabel.text = offer.firstCyclePrice // "Free", "$1.99" or "$59.99"
// "Save 50%" badge — compares base offers, so trials don't distort it
val savings = billing.getSavingsPercent("premium_monthly", "premium_yearly")
savingsBadge.isVisible = savings > 0
savingsBadge.text = "Save $savings%"
// Only promise a trial the user can actually claim
subscribeButton.text = if (billing.isEligibleForFreeTrial("premium_sub")) {
"Start free trial"
} else {
"Subscribe"
}Localization:
BillingPeriod.formatOf()is an English convenience. For shipped UI useBillingPeriod.parse(iso)and pairunit+countwith your own plurals resources.
One-time products can carry multiple offers too (discounts, rentals, pre-orders, limited quantity):
billing.getBestOneTimeOffer("remove_ads")?.let { offer ->
price.text = offer.formattedPrice
badge.isVisible = offer.isDiscounted
badge.text = "-${offer.effectiveDiscountPercent}%"
buyButton.isEnabled = !offer.isSoldOut && offer.isValidAt()
buyButton.setOnClickListener { billing.purchase(activity, offer) }
}// Check subscription state
val state = AppPurchase.getInstance().getSubscriptionState("premium_monthly")
when (state) {
SubscriptionState.ACTIVE -> showPremiumUI()
SubscriptionState.CANCELLED -> showRenewalPrompt() // Still has access
SubscriptionState.ON_HOLD -> showFixPaymentPrompt() // Payment declined — no access (v4.4.0+)
SubscriptionState.EXPIRED -> showSubscribeButton()
}
// Upgrade subscription
AppPurchase.getInstance().upgradeSubscription(activity, "premium_yearly")
// Downgrade subscription
AppPurchase.getInstance().downgradeSubscription(activity, "premium_basic")
// Full control with proration mode
AppPurchase.getInstance().changeSubscription(
activity,
"premium_monthly",
"premium_yearly",
SubscriptionReplacementMode.CHARGE_PRORATED_PRICE
)When Play cannot charge a subscriber, the subscription enters account hold. As of 4.4.0 this is detected client-side — earlier versions needed a server round-trip.
if (billing.hasSubscriptionOnHold()) {
// Let Play walk the user through fixing their payment method.
billing.showInAppMessages(activity, object : InAppMessageListener {
override fun onSubscriptionRecovered(purchaseToken: String) {
refreshPremiumUi() // isPurchased() is already up to date here
}
override fun onNoActionNeeded() { }
})
}Behavior change:
isSubscriptionActive()returnsfalseduring account hold, matching Google's requirement. If your app deliberately keeps serving these users, checkisSuspended()explicitly.
billing.setProductDetailsListener(object : ProductDetailsListener {
override fun onProductDetailsLoaded(
productType: String,
loaded: List<ProductDetails>,
unfetched: List<UnfetchedProduct>,
) {
unfetched.forEach { Log.e("Billing", "${it.productId}: status ${it.statusCode}") }
}
override fun onProductDetailsFailed(productType: String, responseCode: Int, debugMessage: String?) { }
})Register it before initBilling. Products land in unfetched when the id is misspelled, the product is inactive in Play Console, or the account can't see the release track.
- NativeTemplateView Guide
- Ad Loading Strategies
- Jetpack Compose Integration
- Native Ads Caching
- Interstitial Ads
- Rewarded Ads
- App Open Ads
- Multi-Provider Waterfall
- Yandex Integration
- Billing Integration Guide
- Release Notes v4.4.5
- Release Notes v4.4.4
- Release Notes v4.4.3
- Release Notes v4.4.2
- Release Notes v4.4.1
- Release Notes v4.4.0
- Release Notes v4.3.5
- Release Notes v4.3.4
- Release Notes v4.3.3
- Release Notes v4.3.2
- Release Notes v4.3.1
- Release Notes v4.3.0
- Release Notes v4.2.0
- Release Notes v3.6.0
- Release Notes v3.5.9
- Release Notes v3.5.8
- Release Notes v3.5.7
- Release Notes v3.4.6
- Release Notes v3.4.5
- Release Notes v3.4.4
- Release Notes v3.4.3
- Release Notes v3.4.2
- Release Notes v3.4.1
- Release Notes v3.4.0
- Release Notes v3.3.9
- Release Notes v3.3.8
- Release Notes v3.3.7
- Release Notes v3.3.6
- Release Notes v3.3.5
- Release Notes v3.3.4
- Release Notes v3.3.3
- Release Notes v3.3.2
- Release Notes v3.3.0
- Release Notes v3.1.0
- Release Notes v3.0.0
- API Reference
- Multi-Provider Waterfall
- Yandex Integration
- Rewarded Ads
- Billing Integration
- Purchase Categories
- Consumable Products
- Subscriptions
- Subscription Offers
- Subscription Upgrades
Online: https://i2hammad.github.io/AdManageKit/
Generate locally:
./gradlew dokkaGenerateHtmlOutput: build/dokka/html/index.html
AdManageKit provides an MCP (Model Context Protocol) server that integrates with AI tools like Claude Code, Claude Desktop, Cursor, and other MCP-compatible clients. It provides 10 tools for documentation lookup and code generation.
| Category | Tools |
|---|---|
| Documentation | search_docs, get_doc_by_topic, get_api_reference, get_release_notes, get_migration_guide, list_documentation |
| Code Generation | generate_config, generate_ad_integration, generate_billing_code, generate_compose_code |
Claude Desktop / Cursor (claude_desktop_config.json):
{
"mcpServers": {
"admanagekit": {
"command": "npx",
"args": ["-y", "admanagekit-mcp-server"]
}
}
}Claude Code (auto-configured via .mcp.json when working in this repo):
{
"mcpServers": {
"admanagekit": {
"command": "node",
"args": ["mcp-server/dist/index.js"]
}
}
}- Search documentation across all docs, wiki pages, and API references
- Look up API references for any class (AdManager, AppOpenManager, NativeAdManager, etc.)
- Generate integration code for any ad type with display modes, loading strategies, and callbacks
- Generate billing code for purchases, subscriptions, consumables, and expiry verification
- Generate Compose code for all Compose ad components
- Supports both Kotlin and Java output
See mcp-server/ for more details.
The Next-Gen GMA SDK swap (see Next-Gen GMA SDK) is source-compatible for most consumers. It only affects you if your code does one of the following:
1. You read specific members of an error/value object, not just call the callback method.
AdKitError/AdKitLoadError now resolve to the Next-Gen SDK's LoadAdError, which drops the old domain/cause fields and replaces the free-form Int error code with a closed LoadAdError.ErrorCode enum:
// Before (4.1.x and earlier)
override fun onFailedToLoad(error: AdKitError?) {
Log.e(TAG, "Failed: ${error?.code} / ${error?.domain}") // .domain no longer exists
}
// After (4.2.0)
override fun onFailedToLoad(error: AdKitError?) {
Log.e(TAG, "Failed: ${error?.code}") // .code is now a LoadAdError.ErrorCode enum, still fine to log
}2. You use the raw-callback overload of AdManager.loadInterstitialAd.
// Before
AdManager.getInstance().loadInterstitialAd(context, adUnitId,
object : com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback() {
override fun onAdLoaded(ad: com.google.android.gms.ads.interstitial.InterstitialAd) { }
override fun onAdFailedToLoad(error: com.google.android.gms.ads.LoadAdError) { }
})
// After
AdManager.getInstance().loadInterstitialAd(context, adUnitId,
object : com.google.android.libraries.ads.mobile.sdk.common.AdLoadCallback<
com.google.android.libraries.ads.mobile.sdk.interstitial.InterstitialAd> {
override fun onAdLoaded(ad: InterstitialAd) { }
override fun onAdFailedToLoad(adError: LoadAdError) { }
})3. You supply a custom native ad layout XML.
<!-- Before -->
<com.google.android.gms.ads.nativead.NativeAdView ...>
<com.google.android.gms.ads.nativead.MediaView android:id="@+id/ad_media" ... />
<com.google.android.gms.ads.nativead.AdChoicesView android:id="@+id/ad_choices_view" ... />
</com.google.android.gms.ads.nativead.NativeAdView>
<!-- After -->
<com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView ...>
<com.google.android.libraries.ads.mobile.sdk.nativead.MediaView android:id="@+id/ad_media" ... />
<com.google.android.libraries.ads.mobile.sdk.common.AdChoicesView android:id="@+id/ad_choices_view" ... />
</com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdView>4. Your Compose callback lambdas' parameter types are explicitly typed.
If you wrote onAdFailedToLoad: (com.google.android.gms.ads.LoadAdError?) -> Unit = { ... } explicitly rather than letting Kotlin infer the type from the composable's signature, update the import to com.google.android.libraries.ads.mobile.sdk.common.LoadAdError.
Everything else:
- Call
MobileAds.initialize()once at app startup if you haven't already — see Next-Gen GMA SDK. AdManageKit doesn't do this for you. - Bump
compileSdkto 37+. - If you relied on
BannerAdView/BannerAdProvider'spause()/resume()actually pausing ad refresh, that's now a no-op — the Next-Gen SDK'sAdViewdoesn't expose it. - Google Play Billing 9.1.0 needs no changes if you only use
AppPurchase/PurchaseItem.
See Release Notes v4.2.0 for the full technical writeup.
Version 3.0.0 is fully backward compatible. Optionally adopt new features:
Replace separate load + show calls with single showOrWaitForAd():
// Before (v2.9.0) - Two-step approach
AdManager.getInstance().loadInterstitialAdForSplash(this, adUnitId, 10_000, object : AdManagerCallback() {
override fun onNextAction() {
AdManager.getInstance().forceShowInterstitial(this@SplashActivity, callback)
}
})
// After (v3.0.0) - Single smart call
AdManager.getInstance().showOrWaitForAd(
activity = this,
callback = object : AdManagerCallback() {
override fun onNextAction() { navigateNext() }
},
timeoutMillis = 10_000
)// Before (single ad unit)
AdManager.getInstance().loadInterstitialAd(context, "single_unit")
// After (multiple ad units for redundancy)
AdManager.getInstance().loadMultipleAdUnits(context, "high_ecpm", "medium_ecpm", "fallback")// Prefetch before external intents
appOpenManager.prefetchNextAd()
startActivityForResult(cameraIntent, REQUEST_CODE)Version 2.9.0 has one breaking change for consumable products:
Consumables are no longer auto-consumed. You must manually call consumePurchase():
// Before v2.9.0 (auto-consume)
AppPurchase.getInstance().setConsumePurchase(true) // Deprecated
// After v2.9.0 (manual consume)
AppPurchase.getInstance().setPurchaseHistoryListener(object : PurchaseHistoryListener {
override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
grantItems(productId, purchase.quantity)
AppPurchase.getInstance().consumePurchase(productId) // Manual!
}
override fun onPurchaseConsumed(productId: String, purchase: PurchaseResult) { }
})Use Purchase Categories for better product classification:
// Before
PurchaseItem("coins", TYPE_IAP.PURCHASE, true) // isConsumable
// After (explicit categories)
PurchaseItem("coins", TYPE_IAP.PURCHASE, PurchaseCategory.CONSUMABLE)
PurchaseItem("remove_ads", TYPE_IAP.PURCHASE, PurchaseCategory.REMOVE_ADS)Version 2.8.0 is fully backward compatible with one behavioral change:
forceShowInterstitial() now respects loading strategy:
// If you need old behavior (always force fetch), use:
AdManager.getInstance().forceShowInterstitialAlways(activity, callback)
// Or set strategy to ON_DEMAND globally:
AdManageKitConfig.interstitialLoadingStrategy = AdLoadingStrategy.ON_DEMANDVersion 2.7.0 is fully backward compatible. Optionally adopt new features:
// Old way (still works)
val nativeBannerMedium = NativeBannerMedium(context)
nativeBannerMedium.loadNativeBannerAd(activity, adUnitId)
// New unified approach
val nativeTemplateView = NativeTemplateView(context)
nativeTemplateView.setTemplate(NativeAdTemplate.CARD_MODERN)
nativeTemplateView.loadNativeAd(activity, adUnitId)The app module demonstrates all features. To run:
- Clone:
git clone https://github.com/i2hammad/AdManageKit.git - Open in Android Studio
- Replace placeholder AdMob IDs
- Run on device or emulator
- Fork the repository
- Create a branch (
git checkout -b feature/YourFeature) - Commit changes (
git commit -m 'Add YourFeature') - Push (
git push origin feature/YourFeature) - Open a Pull Request
Licensed under the MIT License. See LICENSE.
For issues: GitHub Issues or hammadmughal0001@gmail.com



