This 10-minute guide walks you through integrating Android in-app purchases with Jetpack Compose, covering everything from SDK setup to paywall implementation. You'll learn to handle purchases, restore transactions, and track premium users with minimal code and maximum reliability.
This guide will walk you through adding Android in-app purchases to your app using Jetpack Compose in under 10 minutes. I'll use Adapty to handle in-app purchases and Koin for dependency injection.
Quick start option: If you prefer to code along without diving deep into each section right now, you can copy the code blocks directly into Cursor and build as you go. I’ve tested it multiple times, and it just works.
Step 1: Set up dependencies
First, add the required dependencies to your project.**
**In libs.versions.toml:
Step 3: Create the Koin module and subscription service
A Koin module defines how to create and provide dependencies throughout your app. Create a new file called AppModule.kt in a di folder (di stands for dependency injection):
package com.yourpackagename.di // Change to your app's package nameimport com.yourpackagename.services.SubscriptionService import org.koin.dsl.moduleval appModule = module { single { SubscriptionService(get()) }}
Kotlin
Create services/SubscriptionService.kt:
package com.yourpackagename.servicesimport android.app.Activityimport android.content.Contextimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport io.adapty.Adaptyimport io.adapty.models.AdaptyPaywallimport io.adapty.models.AdaptyProfileimport io.adapty.utils.AdaptyResultimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.asStateFlowclass SubscriptionService(private val context: Context) { var paywall by mutableStateOf<AdaptyPaywall?>(null) private set var paywallErrorText by mutableStateOf<String?>(null) private set private val _isUserPremium = MutableStateFlow(false) val isUserPremium: StateFlow<Boolean> = _isUserPremium.asStateFlow() var showPaywall by mutableStateOf(false) private set private var currentProfile: AdaptyProfile? = null fun getProfile() { Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { currentProfile = result.value _isUserPremium.value = checkPremiumStatus(result.value) if (_isUserPremium.value) { showPaywall = false } } is AdaptyResult.Error -> { println("Error: ${result.error.message}") } } } } fun getPaywall(placementId: String) { Adapty.getPaywall(placementId) { result -> when (result) { is AdaptyResult.Success -> { paywall = result.value paywallErrorText = null } is AdaptyResult.Error -> { paywallErrorText = result.error.message println("Error: ${result.error.message}") } } } } fun identifyUser(userId: String) { Adapty.identify(userId) { error -> if (error != null) { println("Could not identify user: ${error.message}") } } } fun makePurchase(activity: Activity, productVendorId: String) { paywall?.let { currentPaywall -> Adapty.getPaywallProducts(currentPaywall) { result -> when (result) { is AdaptyResult.Success -> { val productToBuy = result.value.find { it.vendorProductId == productVendorId } if (productToBuy != null) { Adapty.makePurchase(activity, productToBuy) { purchaseResult -> when (purchaseResult) { is AdaptyResult.Success -> { when (val adaptypurchaseResult = purchaseResult.value) { is AdaptyPurchaseResult.Success -> { currentProfile = adaptypurchaseResult.profile _isUserPremium.value = checkPremiumStatus(adaptypurchaseResult.profile) if (_isUserPremium.value) { showPaywall = false } println("Purchase successful!") } is AdaptyPurchaseResult.UserCanceled -> { println("User canceled purchase") } is AdaptyPurchaseResult.Pending -> { println("Purchase is pending") } } } is AdaptyResult.Error -> { println("Failed: ${purchaseResult.error.message}") } } } } else { println("Product not found to buy.") } } is AdaptyResult.Error -> { println("Failed to get products: ${result.error.message}") } } } } } fun restorePurchases() { Adapty.restorePurchases { result -> when (result) { is AdaptyResult.Success -> { currentProfile = result.value _isUserPremium.value = checkPremiumStatus(result.value) if (_isUserPremium.value) { showPaywall = false } println("Restore successful!") } is AdaptyResult.Error -> { println("Restore failed: ${result.error.message}") } } } } fun setPaywallVisibility(isVisible: Boolean) { showPaywall = isVisible } private fun checkPremiumStatus(profile: AdaptyProfile?): Boolean { return profile?.accessLevels?.get("premium")?.isActive == true }}
Kotlin
Step 4: Create the paywall screen using Adapty's built-in UI
Adapty provides a ready-to-use AdaptyPaywallView composable that handles the entire paywall experience. Here's how to implement it:
The SubscriptionService already handles purchase logic, so you only need to call makePurchase() and the service will automatically update the premium status.
You've successfully integrated Adapty subscriptions into your Jetpack Compose app!
Important configuration steps
Before testing your implementation, make sure to:
Replace placeholder values:
"YOUR_PUBLIC_SDK_KEY" in MyApplication.kt with your actual Adapty Public SDK Key
"YOUR_PAYWALL_PLACEMENT_ID" with your actual placement ID from Adapty dashboard
Update access level name:
In the checkPremiumStatus() function, change "premium" to match your access level name configured in Adapty.
Set up Google Play Console:
Ensure your in-app products are properly configured in Google Play Console
Test with a signed APK or AAB file
Error handling:
Consider implementing user-friendly error messages for purchase failures
Add loading states for better user experience
Testing:
Test thoroughly on physical devices
Use Google Play Console's testing tools to verify purchase flows
What's next?
This implementation provides a solid foundation for subscription management in your Android app. Adapty handles the complex subscription logic, while Koin keeps your code organized and maintainable.
Most apps send every install to the same onboarding and the same paywall, no matter the intentBut matching the flow to the ad source (TikTok, Apple ,Meta) showed a significant uplift. Here's how you can do it in the Flow & paywall builder.
A developer's framework for choosing between native and WebView paywall rendering: latency, accessibility, platform fidelity, and what each trade-off actually costs.