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"
inMyApplication.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:
- Custom paywall designs that match your app’s branding
- Analytics integration to track conversion rates
- A/B testing different paywalls
- Promotional offers and pricing experiments