How to add Android in-app purchases to your app in 10 minutes

June 4, 2025 
by 
Add Subscriptions To Your Android App With Adapty In 10 Minutes 2025 Guide Preview 1

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:

[versions]
adaptyBom = "3.4.0"

[libraries]
adapty-bom = { module = "io.adapty:adapty-bom", version.ref = "adaptyBom" }
adapty = { module = "io.adapty:android-sdk" }
adapty-ui = { module = "io.adapty:android-ui" }

In build.gradle.kts (module-level):

dependencies {
    implementation(platform(libs.adapty.bom))
    implementation(libs.adapty)
    implementation(libs.adapty.ui)
    
    // Koin for DI
    implementation("io.insert-koin:koin-android:3.5.3")
    implementation("io.insert-koin:koin-androidx-compose:3.5.3")
}

After adding these dependencies, Android Studio will display a sync notification bar at the top. Click “Sync Now” to proceed.

Step 2: Initialize Koin in your Application class

Create an Application class if you don’t already have one. This will be named something like MyApplication.kt:

package com.yourpackagename

package com.yourpackagename

import android.app.Application
import com.yourpackagename.services.SubscriptionService
import io.adapty.Adapty
import io.adapty.AdaptyConfig
import io.adapty.utils.AdaptyLogLevel
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import org.koin.dsl.module

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        startKoin {
            androidContext(this@MyApplication)
            modules(appModule)
        }
        

        Adapty.logLevel = AdaptyLogLevel.VERBOSE
        

        Adapty.activate(
            applicationContext,
            AdaptyConfig.Builder("YOUR_PUBLIC_SDK_KEY")
                .withObserverMode(false)
                .withIpAddressCollectionDisabled(false)
                .withAdIdCollectionDisabled(false)
                .build()
        )
    }
}

Important: You must register this Application class in your AndroidManifest.xml. Add the android:name attribute to your <application> tag:

<application
 android:name=".MyApplication"
 >
</application>

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 name

import com.yourpackagename.services.SubscriptionService 
import org.koin.dsl.module

val appModule = module {
    single { SubscriptionService(get()) }
}

Create services/SubscriptionService.kt:

package com.yourpackagename.services

import android.app.Activity
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import io.adapty.Adapty
import io.adapty.models.AdaptyPaywall
import io.adapty.models.AdaptyProfile
import io.adapty.utils.AdaptyResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

class 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
    }
}

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:

Create ui/composables/ManageSubscriptionScreen.kt:

import android.app.Activity
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import com.adapty.Adapty
import com.adapty.errors.AdaptyError
import com.adapty.models.AdaptyPaywallProduct
import com.adapty.models.AdaptyProfile
import com.adapty.models.AdaptyPurchaseResult
import com.adapty.models.AdaptySubscriptionUpdateParameters
import com.adapty.ui.AdaptyPaywallScreen
import com.adapty.ui.AdaptyUI
import com.adapty.ui.listeners.AdaptyUiDefaultEventListener
import com.adapty.ui.listeners.AdaptyUiEventListener
import com.adapty.utils.AdaptyResult
import com.yourpackagename.services.SubscriptionService
import kotlinx.coroutines.launch
import org.koin.androidx.compose.get

@Composable
fun ManageSubscriptionScreen(
    subscriptionService: SubscriptionService = get(),
    placementId: String = "YOUR_PAYWALL_PLACEMENT_ID"
) {
    val activity = LocalContext.current as Activity
    val coroutineScope = rememberCoroutineScope()
    val paywall = subscriptionService.paywall
    val showPaywallView = subscriptionService.showPaywall
    val isPremium by subscriptionService.isUserPremium.collectAsState()
    val products = remember { mutableStateOf<List<AdaptyPaywallProduct>?>(null) }
    val viewConfiguration = remember { mutableStateOf<AdaptyUI.LocalizedViewConfiguration?>(null) }
    
    LaunchedEffect(key1 = placementId) {
        if (!isPremium) {
            subscriptionService.getPaywall(placementId)
        }
    }
    
    // Load products and view configuration when paywall is available
    LaunchedEffect(paywall) {
        paywall?.let { currentPaywall ->
            // Load products
            Adapty.getPaywallProducts(currentPaywall) { result ->
                when (result) {
                    is AdaptyResult.Success -> {
                        products.value = result.value
                    }
                    is AdaptyResult.Error -> {
                        println("Error loading products: ${result.error.message}")
                    }
                }
            }
            
            // Load view configuration
            AdaptyUI.getViewConfiguration(currentPaywall) { result ->
                when (result) {
                    is AdaptyResult.Success -> {
                        viewConfiguration.value = result.value
                    }
                    is AdaptyResult.Error -> {
                        println("Error loading view config: ${result.error.message}")
                    }
                }
            }
        }
    }
    
    if (showPaywallView && paywall != null && viewConfiguration.value != null && !isPremium) {
        AdaptyPaywallScreen(
            viewConfiguration = viewConfiguration.value!!,
            products = products.value,
            eventListener = object : AdaptyUiEventListener {
                override fun onActionPerformed(action: AdaptyUI.Action, context: Context) {
                    when (action) {
                        is AdaptyUI.Action.Close -> {
                            subscriptionService.setPaywallVisibility(false)
                        }
                        else -> {
                            // Handle other actions if needed
                        }
                    }
                }
                
                override fun onAwaitingSubscriptionUpdateParams(
                    product: AdaptyPaywallProduct,
                    context: Context,
                    onSubscriptionUpdateParamsReceived: AdaptyUiEventListener.SubscriptionUpdateParamsCallback
                ) {
                    // For simple implementation, call with null (no subscription update)
                    onSubscriptionUpdateParamsReceived.invoke(null)
                }
                
                override fun onLoadingProductsFailure(
                    error: AdaptyError,
                    context: Context
                ): Boolean {
                    println("Loading products failed: ${error.message}")
                    return false // Don't retry
                }
                
                override fun onProductSelected(product: AdaptyPaywallProduct, context: Context) {
                    println("Product selected: ${product.vendorProductId}")
                }
                
                override fun onPurchaseStarted(product: AdaptyPaywallProduct, context: Context) {
                    println("Purchase started: ${product.vendorProductId}")
                }
                
                override fun onPurchaseFinished(
                    purchaseResult: AdaptyPurchaseResult,
                    product: AdaptyPaywallProduct,
                    context: Context
                ) {
                    when (purchaseResult) {
                        is AdaptyPurchaseResult.Success -> {
                            subscriptionService.setPaywallVisibility(false)
                            coroutineScope.launch {
                                subscriptionService.getProfile()
                            }
                            println("Purchase successful: ${product.vendorProductId}")
                        }
                        is AdaptyPurchaseResult.UserCanceled -> {
                            println("Purchase canceled by user")
                        }
                        is AdaptyPurchaseResult.Pending -> {
                            println("Purchase is pending")
                        }
                    }
                }
                
                override fun onPurchaseFailure(
                    error: AdaptyError,
                    product: AdaptyPaywallProduct,
                    context: Context
                ) {
                    println("Purchase failed: ${error.message}")
                }
                
                override fun onRestoreStarted(context: Context) {
                    println("Restore started")
                }
                
                override fun onRestoreSuccess(profile: AdaptyProfile, context: Context) {
                    subscriptionService.setPaywallVisibility(false)
                    coroutineScope.launch {
                        subscriptionService.getProfile()
                    }
                    println("Restore successful")
                }
                
                override fun onRestoreFailure(error: AdaptyError, context: Context) {
                    println("Restore failed: ${error.message}")
                }
                
                override fun onRenderingError(error: AdaptyError, context: Context) {
                    println("Paywall rendering error: ${error.message}")
                    subscriptionService.setPaywallVisibility(false)
                }
            }
        )
    }
}
}

Step 5: Integrate paywall into your main activity

Update your MainActivity.kt to include the subscription functionality:

package com.yourpackagename

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.yourpackagename.services.SubscriptionService
import com.yourpackagename.ui.composables.ManageSubscriptionScreen
import org.koin.androidx.compose.get

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            YourAppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    ManageSubscriptionScreen(placementId = "YOUR_PAYWALL_PLACEMENT_ID")
                    MainContentView()
                }
            }
        }
    }
}

@Composable
fun MainContentView(subscriptionService: SubscriptionService = get()) {
    val isPremium by subscriptionService.isUserPremium.collectAsState()
    
    LaunchedEffect(Unit) {
        subscriptionService.getProfile()
    }
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = if (isPremium) "Welcome Premium User!" else "Hello, World!",
            style = MaterialTheme.typography.headlineMedium,
            modifier = Modifier.padding(bottom = 20.dp)
        )
        
        if (!isPremium) {
            Button(onClick = {
                subscriptionService.setPaywallVisibility(true)
            }) {
                Text("Unlock Premium Features")
            }
        } else {
            Text("You have all premium features!")
        }
        
        Button(
            onClick = { subscriptionService.restorePurchases() },
            modifier = Modifier.padding(top = 16.dp)
        ) {
            Text("Restore Purchases")
        }
    }
}

Alternative: Build your own custom paywall UI

If you prefer creating a custom paywall interface instead of using Adapty’s built-in UI, here’s how to implement it:

1. Access Products manually

Your SubscriptionService already loads the paywall. Access products like this:

val products = paywall?.products.orEmpty()

2. Create custom paywall UI

In your Composable, build your own UI using paywall?.products.

@Composable
fun CustomPaywallScreen(subscriptionService: SubscriptionService = get()) {
    val activity = LocalContext.current as Activity
    val paywall = subscriptionService.paywall
    val isPremium by subscriptionService.isUserPremium.collectAsState()

    if (paywall != null && !isPremium) {
        Column {
            paywall.products.forEach { product ->
                Button(onClick = {
                    subscriptionService.makePurchase(
                        activity = activity,
                        paywall = paywall,
                        productVariationId = product.variationId
                    )
                }) {
                    Text("Buy ${product.localizedTitle} - ${product.localizedPrice}")
                }
            }

            Button(onClick = {
                subscriptionService.restorePurchases()
            }) {
                Text("Restore Purchases")
            }
        }
    } else {
        Text("Loading paywall or you're already Premium")
    }
}

3. Handle purchases

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.

Consider enhancing your implementation with:








Burak Ilhan
DevRel at Adapty
Android
Tutorial

On this page

Ready to create your first paywall with Adapty?
Build money-making paywalls without coding
Get started for free