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 3.3.4 adds Subscription Expiry Verification via server-side API for accurate expiry dates and remaining days.
Beta Notice: The Next-Gen GMA SDK (
com.google.android.libraries.ads.mobile.sdk) is currently in beta by Google. While AdManageKit's nextgen branch is stable for production use, the underlying Google SDK may receive breaking changes until it reaches stable release.
AdManageKit offers a Next-Gen GMA SDK version on the nextgen branch, featuring Google's modern preloader-based ad loading system.
| Feature | Main Branch (GMS SDK) | Next-Gen Branch |
|---|---|---|
| SDK | play-services-ads (stable) |
ads-mobile-sdk (beta) |
| Ad Loading | Traditional load/show | Preloader-based with auto-refill |
| Threading | Manual main thread dispatch | Automatic background thread safety |
| Buffer System | N/A | Configurable ad buffers per type |
| Background Handling | Basic | Smart pending ad queue |
- Preloader System: SDK automatically loads next ad after one is consumed
- Background-Aware Ads: App open ads won't show when app is in background
- Pending Ad Queue: Ads that load while backgrounded are saved for return
- Configurable Buffers: Set how many ads to keep ready per type
// Next-Gen preloader configuration
AdManageKitConfig.apply {
enableInterstitialPreloader = true
enableAppOpenPreloader = true
interstitialPreloaderBufferSize = 2
}Both branches use the same callback signatures via type aliases:
AdKitError→ resolves to appropriate SDK error typeAdKitLoadError→ resolves to appropriate SDK load error typeAdKitValue→ resolves to appropriate SDK value type
Your callback implementations work on both branches without changes.
| Use Case | Recommended |
|---|---|
| Production apps (stable) | Main branch (v3.3.4) |
| New projects wanting latest features | Nextgen branch (v4.1.1) |
| Testing preloader system | Nextgen branch |
| Risk-averse production | Main branch |
- Server-Side Verification: New API to verify subscriptions and get accurate expiry dates from your backend
- Expiry Methods:
getExpiryTimeFormatted(),getRemainingDays(),isExpired()on PurchaseResult - AppPurchase Helpers:
getSubscriptionExpiryTime(),getSubscriptionRemainingDays(),isSubscriptionExpired()
// Set up verification callback
AppPurchase.getInstance().setSubscriptionVerificationCallback { packageName, subscriptionId, purchaseToken, listener ->
yourApi.verifySubscription(purchaseToken) { expiryMillis ->
val details = SubscriptionVerificationCallback.SubscriptionDetails.Builder()
.setExpiryTimeMillis(expiryMillis)
.build()
listener.onVerified(details)
}
}
// Verify and get expiry
AppPurchase.getInstance().verifySubscription("premium_monthly",
object : AppPurchase.SubscriptionVerificationListener {
override fun onVerified(subscription: PurchaseResult) {
val expiryDate = subscription.getExpiryTimeFormatted("dd MMM yyyy")
val daysLeft = subscription.getRemainingDays()
}
override fun onVerificationFailed(error: String?) { }
}
)- Migration Compatibility: Callbacks use
AdKitError,AdKitLoadError,AdKitValuetype aliases - Same Signatures: Your callback implementations work across both SDK versions
- Easy Migration: Switch between main (GMS SDK) and nextgen (Next-Gen SDK) branches without code changes
// Callbacks now use type aliases that resolve to the appropriate SDK types
object : AdLoadCallback() {
override fun onFailedToLoad(error: AdKitError?) { // Works on both branches
Log.e("Ads", "Failed: ${error?.message}")
}
override fun onPaidEvent(adValue: AdKitValue) { // Works on both branches
trackRevenue(adValue.valueMicros)
}
}- Ad Unit Assignment: Fixed ad unit not being assigned to AdManager on first HYBRID fetch
- Immediate Availability:
adUnit()now sets AdManager.adUnitId immediately for first-call reliability
- Counter Persistence: Call counter now persists across builder instances in AdManager
- Counter API: New methods to manage counters:
getCallCount(),resetCallCount(),resetAllCallCounts()
// everyNthTime now works correctly
InterstitialAdBuilder.with(activity)
.adUnit(adUnitId)
.everyNthTime(3) // Shows on 3rd, 6th, 9th calls, etc.
.show { navigateNext() }
// Reset counters when user upgrades
AdManager.getInstance().resetAllCallCounts()- flexible: Adaptive layout that adjusts to available space
- icon_left: Icon on left side with MediaView at top for GridView display
- top_icon_media: Icon at top, MediaView in middle, CTA at bottom
nativeTemplateView.setTemplate(NativeAdTemplate.FLEXIBLE)
nativeTemplateView.setTemplate(NativeAdTemplate.ICON_LEFT)
nativeTemplateView.setTemplate(NativeAdTemplate.TOP_ICON_MEDIA)- Auto-Caching: Successfully loaded ads are now properly cached for future fallback
- RecyclerView Optimized: Fresh ads build up the cache over time for better fallback availability
- Complete Implementation: Strategy now works as documented
- MEDIUM_HORIZONTAL: 55% media (left) / 45% content (right) horizontal split layout
- 27 Total Templates: 21 standard + 6 video templates
- Multiple Ad Units: Load multiple interstitial ad units into a pool for maximum show rate
- Auto-Selection: Shows ANY available ad from the pool when requested
- Duplicate Prevention: Automatically skips duplicate load requests
- showOrWaitForAd(): Single method handles all splash scenarios automatically
- Intelligent Behavior: Shows cached ad immediately, waits if loading, or fetches fresh
- prefetchNextAd(): Prefetch ads before external intents for instant display on return
- isAdLoading(): Check if ad is currently being fetched
- Session Tracking: Fill rate, show rate, and impression tracking
- getAdStats(): Access session-level ad performance metrics
- Modern
WindowInsetsControllerAPI (replaces deprecated systemUiVisibility) - Thread-safe ad pool with
ConcurrentHashMap - Cross-ad-unit fallback for native ads
| 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:
| Main Branch (Stable GMS SDK) | Next-Gen Branch (Beta GMA SDK) |
|---|---|
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit:v3.3.4'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-billing:v3.3.4'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-core:v3.3.4'
// For Jetpack Compose support
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-compose:v3.3.4' |
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-nextgen:v4.1.1'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-billing-nextgen:v4.1.1'
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-core-nextgen:v4.1.1'
// For Jetpack Compose support
implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-compose-nextgen:v4.1.1' |
Step 3: Sync your project with Gradle.
- 27 Template Styles: card_modern, material3, app_store, social_feed, gradient_card, pill_banner, medium_horizontal, flexible, icon_left, top_icon_media, spotlight, and more
- 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: 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
- UMP consent management (GDPR/CCPA)
- Automatic ad hiding for purchased users
- Core Module: Shared interfaces and configuration
- Compose Module: Jetpack Compose integration
- Billing Module: Google Play Billing Library v8
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 interstitial after showing (v2.7.0+)
interstitialAutoReload = true // default: true
}
// Set up billing
BillingConfig.setPurchaseProvider(BillingPurchaseProvider())
// Initialize app open ads
appOpenManager = AppOpenManager(this, "your-app-open-ad-unit-id")
}
}<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" />bannerAdView.loadBanner(this, "ca-app-pub-xxx/yyy")
// Collapsible banner
bannerAdView.loadCollapsibleBanner(this, "ca-app-pub-xxx/yyy", true)<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
// 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) { }
})// Check subscription state
val state = AppPurchase.getInstance().getSubscriptionState("premium_monthly")
when (state) {
SubscriptionState.ACTIVE -> showPremiumUI()
SubscriptionState.CANCELLED -> showRenewalPrompt() // Still has access
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
)- NativeTemplateView Guide
- Ad Loading Strategies
- Jetpack Compose Integration
- Native Ads Caching
- Interstitial Ads
- App Open Ads
- Billing Integration Guide
- 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
Online: https://i2hammad.github.io/AdManageKit/
Generate locally:
./gradlew dokkaHtmlMultiModuleOutput: build/dokka/htmlMultiModule/index.html
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 [email protected]



