Quickstart Adapty setup guide: Android

Last updated September 8, 2025 
by 
Quickstart Guide Android

Get from zero Adapty setup to displaying a paywall and processing purchases in under 60 minutes.

Pre-requisites

Before you begin, make sure you have:

  • A working Android app with Kotlin and Jetpack Compose
  • Android Studio with minimum SDK 24 or later
  • Google Play Console setup for in-app purchases
  • At least one in-app purchase product created in Google Play Console
  • An Adapty account with your app added to the dashboard, including at least one product, paywall, and placement

Project files

To follow along with this tutorial, download the project files from GitHub. The starter branch contains the app without Adapty integration and a mocked purchase experience, while the main branch contains the finished project.

Install the Adapty SDK in your project

Add the following to the versions section of the libs.versions.toml file:

[versions]
adaptyBom = "3.10.0"

Then, in the same file, add the following definitions to the Libraries section:

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

And finally, add the Adapty Android SDK to your app-level build.gradle.kts file:

dependencies {
    // ...Other dependencies

    // Adapty SDK
    implementation(platform(libs.adapty.bom))
    implementation(libs.adapty.android.sdk)
}

Sync your project to install the dependencies.

Initialize Adapty in your app

First, add these constants to your utils/AppConstants.kt file:

package io.adapty.FocusJournal.utils

object AppConstants {
    object Adapty {
        const val API_KEY = "API key goes here"
        const val ACCESS_LEVEL_ID = "premium"
        const val PLACEMENT_ID = "on_tap_history"
    }
}

🔐 Find your public SDK key: Adapty Dashboard > Settings > API Keys

Next, initialize Adapty in your FocusJournalApplication.kt file:

package io.adapty.FocusJournal

// ...Existing imports
import com.adapty.Adapty
import com.adapty.models.AdaptyConfig.Builder
import io.adapty.focusjournal.utils.AppConstants

class FocusJournalApplication : Application() {

    // ...

    override fun onCreate() {
        super.onCreate()

        // Initialize Adapty SDK
        Adapty.activate(this, Builder(AppConstants.Adapty.API_KEY).build())
    }
}

This call configures and activates the Adapty SDK when your app launches.

Set up profile handling

Next, edit the ProfileManager.kt file to add Adapty integration.

The primary changes are the addition of the Adapty profile property, the call to refresh the profile, and updating premium status based on the profile’s access levels.

package io.adapty.FocusJournal.viewmodel

// Add these import statements
import com.adapty.Adapty
import com.adapty.models.AdaptyProfile
import com.adapty.utils.AdaptyResult
import io.adapty.focusjournal.utils.AppConstants
import javax.inject.Inject
import javax.inject.Singleton

// Update class to act as a singleton
// and add @Inject constructor after class name
@Singleton
class ProfileManager @Inject constructor(
    private val journalRepository: JournalRepository
) : ViewModel() {

    // Add the customer profile and isLoading values
    private val _customerProfile = MutableStateFlow<AdaptyProfile?>(null)
    val customerProfile: StateFlow<AdaptyProfile?> = _customerProfile.asStateFlow()

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    // Update the existing isPremium declaration to pull data from the profile
    val isPremium: StateFlow<Boolean> = customerProfile.map { profile ->
        profile?.accessLevels?.get(AppConstants.Adapty.ACCESS_LEVEL_ID)?.let { accessLevel ->
            accessLevel.isActive || accessLevel.isInGracePeriod || accessLevel.isLifetime
        } ?: false
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = false
    )

    // Add an init function to refresh the profile immediately
    init {
        refreshProfile()
    }
    
    // Delete this function as it's no longer needed
    fun mockPurchasePremium() {
      // ...
    }

    // Add this function to update the profile from Adapty's servers
    fun refreshProfile() {
        viewModelScope.launch {
            _isLoading.value = true
            Adapty.getProfile { result ->
                when (result) {
                    is AdaptyResult.Success -> {
                        val profile = result.value
                        _customerProfile.value = profile
                    }
                    is AdaptyResult.Error -> {
                        _customerProfile.value = null
                    }
                }
            }
            _isLoading.value = false
        }
    }
    
    // Add this function to update the profile after the user performs a purchase
    fun subscriptionPurchased(profile: AdaptyProfile) {
        _customerProfile.value = profile
    }
}

Create the paywall screen

Update your PaywallScreen.kt file to integrate with Adapty’s purchase flow. This will replace the mocked purchase experience from the starter branch for a proper paywall with elements loaded from Adapty’s SDK:

Note: In the starter branch, you’ll find an updateDialog composable view function. Simply delete the entire function and replace it with the PaywallScreen, FeatureItem, and ProductButton composable view functions.

package io.adapty.focusjournal.ui.screen

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalContext
import android.app.Activity
import com.adapty.Adapty
import com.adapty.models.AdaptyPaywallProduct
import com.adapty.models.AdaptyPurchaseParameters
import com.adapty.models.AdaptyPurchaseResult
import com.adapty.utils.AdaptyResult
import io.adapty.focusjournal.utils.AppConstants
import io.adapty.focusjournal.viewmodel.ProfileManager
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PaywallScreen(
    profileManager: ProfileManager,
    onDismiss: () -> Unit,
    onPurchaseSuccess: () -> Unit
) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    var isLoading by remember { mutableStateOf(false) }
    var errorMessage by remember { mutableStateOf<String?>(null) }
    var products by remember { mutableStateOf<List<AdaptyPaywallProduct>>(emptyList()) }
    var isLoadingPaywall by remember { mutableStateOf(true) }

    // Fetch paywall and products on component load
    LaunchedEffect(Unit) {
        Adapty.getPaywall(AppConstants.Adapty.PLACEMENT_ID, locale = "en") { result ->
            when (result) {
                is AdaptyResult.Success -> {
                    val fetchedPaywall = result.value

                    // Fetch products for this paywall
                    Adapty.getPaywallProducts(fetchedPaywall) { productsResult ->
                        when (productsResult) {
                            is AdaptyResult.Success -> {
                                products = productsResult.value
                                isLoadingPaywall = false
                            }
                            is AdaptyResult.Error -> {
                                errorMessage = "Failed to load products: ${productsResult.error.message}"
                                isLoadingPaywall = false
                            }
                        }
                    }
                }
                is AdaptyResult.Error -> {
                    errorMessage = "Failed to load paywall: ${result.error.message}"
                    isLoadingPaywall = false
                }
            }
        }
    }

    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        // Top Bar
        TopAppBar(
            title = { Text("Premium Features") },
            navigationIcon = {
                IconButton(onClick = onDismiss) {
                    Icon(Icons.Default.Close, contentDescription = "Close")
                }
            }
        )

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(24.dp)
                .verticalScroll(rememberScrollState()),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(24.dp)
        ) {
            // Premium Icon
            Icon(
                Icons.Default.Star,
                contentDescription = null,
                modifier = Modifier.size(64.dp),
                tint = MaterialTheme.colorScheme.primary
            )

            // Title
            Text(
                text = "Unlock Premium Features",
                fontSize = 28.sp,
                fontWeight = FontWeight.Bold,
                textAlign = TextAlign.Center
            )

            // Features List
            Column(
                verticalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                FeatureItem(
                    icon = Icons.Default.List,
                    title = "Journal History",
                    description = "Access all your past journal entries anytime"
                )
                
                FeatureItem(
                    icon = Icons.Default.Star,
                    title = "Premium Support",
                    description = "Get priority customer support"
                )
            }

            Spacer(modifier = Modifier.height(16.dp))

            // Products Section
            if (isLoadingPaywall) {
                Box(
                    modifier = Modifier.fillMaxWidth(),
                    contentAlignment = Alignment.Center
                ) {
                    CircularProgressIndicator()
                }
            } else if (products.isNotEmpty()) {
                Column(
                    modifier = Modifier.fillMaxWidth(),
                    verticalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    products.forEach { product ->
                        ProductButton(
                            product = product,
                            isLoading = isLoading,
                            onClick = {
                                scope.launch {
                                    isLoading = true
                                    errorMessage = null

                                    try {
                                        val activity = context as Activity
                                        val params = AdaptyPurchaseParameters.Builder().build()
                                        Adapty.makePurchase(activity, product, params) { result ->
                                            scope.launch {
                                                when (result) {
                                                    is AdaptyResult.Success -> {
                                                        when (val purchaseResult = result.value) {
                                                            is AdaptyPurchaseResult.Success -> {
                                                                val profile = purchaseResult.profile
                                                                // Notify ProfileManager of successful subscription
                                                                profileManager.subscriptionPurchased(profile)
                                                                // Navigate away from paywall
                                                                onPurchaseSuccess()
                                                            }
                                                            is AdaptyPurchaseResult.UserCanceled -> {
                                                                errorMessage = "Purchase was canceled"
                                                            }
                                                            is AdaptyPurchaseResult.Pending -> {
                                                                errorMessage = "Purchase is pending"
                                                            }
                                                        }
                                                    }
                                                    is AdaptyResult.Error -> {
                                                        errorMessage = "Purchase failed: ${result.error.message}"
                                                    }
                                                }
                                                isLoading = false
                                            }
                                        }
                                    } catch (e: Exception) {
                                        errorMessage = "Purchase failed: ${e.message}"
                                        isLoading = false
                                    }
                                }
                            }
                        )
                    }
                }
            } else {
                Text(
                    text = "No products available",
                    modifier = Modifier.fillMaxWidth(),
                    textAlign = TextAlign.Center,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }

            // Restore Button (Placeholder)
            TextButton(
                onClick = {
                    scope.launch {
                        isLoading = true
                        errorMessage = null
                        
                        // TODO: Implement actual restore logic with correct API
                        kotlinx.coroutines.delay(1000)
                        
                        // For demo purposes
                        errorMessage = "No purchases to restore"
                        isLoading = false
                    }
                },
                enabled = !isLoading
            ) {
                Text("Restore Purchases")
            }

            // Error Message
            errorMessage?.let { message ->
                Card(
                    colors = CardDefaults.cardColors(
                        containerColor = MaterialTheme.colorScheme.errorContainer
                    ),
                    modifier = Modifier.fillMaxWidth()
                ) {
                    Text(
                        text = message,
                        modifier = Modifier.padding(16.dp),
                        color = MaterialTheme.colorScheme.onErrorContainer
                    )
                }
            }

            // Terms and Privacy (placeholder)
            Text(
                text = "By purchasing, you agree to our Terms of Service and Privacy Policy",
                style = MaterialTheme.typography.bodySmall,
                color = MaterialTheme.colorScheme.onSurfaceVariant,
                textAlign = TextAlign.Center
            )
        }
    }
}

@Composable
fun FeatureItem(
    icon: androidx.compose.ui.graphics.vector.ImageVector,
    title: String,
    description: String
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Icon(
            icon,
            contentDescription = null,
            tint = MaterialTheme.colorScheme.primary,
            modifier = Modifier.size(24.dp)
        )
        
        Column(
            verticalArrangement = Arrangement.spacedBy(4.dp)
        ) {
            Text(
                text = title,
                fontWeight = FontWeight.Medium,
                fontSize = 16.sp
            )
            Text(
                text = description,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

@Composable
fun ProductButton(
    product: AdaptyPaywallProduct,
    isLoading: Boolean,
    onClick: () -> Unit
) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Button(
            onClick = onClick,
            enabled = !isLoading,
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
                .height(56.dp),
            colors = ButtonDefaults.buttonColors(
                containerColor = MaterialTheme.colorScheme.primary
            )
        ) {
            if (isLoading) {
                CircularProgressIndicator(
                    modifier = Modifier.size(24.dp),
                    strokeWidth = 2.dp,
                    color = MaterialTheme.colorScheme.onPrimary
                )
            } else {
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    Text(
                        text = product.localizedTitle ?: product.vendorProductId,
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Bold
                    )
                    product.price?.localizedString?.let { price ->
                        Text(
                            text = price,
                            fontSize = 14.sp,
                            color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.9f)
                        )
                    }
                }
            }
        }
    }
}

The above sets up a fully functional paywall screen with Adapty integration. It loads products from your Adapty configuration and handles the complete purchase flow. You also have the option of using our Paywall Builder feature to design and deploy paywalls from the Adapty dashboard without requiring a new app submission.

Update the home screen to show a paywall

Edit your HomeScreen.kt to replace the mock upgrade dialog with a call to the paywall:

package io.adapty.focusjournal.ui.screen

// No import changes

// Add the onShowPaywall parameter
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    profileManager: ProfileManager,
    onNavigateToHistory: () -> Unit,
    onShowPaywall: () -> Unit
) {
    // ...existing values
    
    // Delete this value as it's not needed
    var showUpgradeDialog by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
            .verticalScroll(rememberScrollState()),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        // ...existing UI elements

        // History Button with Premium Gate
        Button(
            onClick = {
                if (isPremium) {
                    onNavigateToHistory()
                } else {
                    // Remove the boolean flip of showUpgradeDialog and replace with:
                    onShowPaywall()
                }
            },
            // ...other modifiers
        ) {
            // ...button contents
        }

        // ...
    }
    
    // Remove the entire if-block as its not needed
    if (showUpgradeDialog) {
      // ...
    }
}

And in the FocusJournalNavigation.kt file, update the arguments to the HomeScreen by adding the route for onShowPaywall. Also

sealed class Screen(val route: String) {
    // ...other screens
    // Add a route to the paywall screen
    object Paywall : Screen("paywall")
}

composable(Screen.Home.route) {
    HomeScreen(
        profileManager = profileManager,
        onNavigateToHistory = {
            navController.navigate(Screen.History.route)
        },
        // Add the following argument with the routing to the Paywall screen
        onShowPaywall = {
            navController.navigate(Screen.Paywall.route)
        }
    )
}
        
// Add this composable for displayig the paywall screen
composable("paywall") {
    PaywallScreen(
        profileManager = profileManager,
        onDismiss = { navController.popBackStack() },
        onPurchaseSuccess = {
            navController.popBackStack()
            // Navigate to history if purchase was successful
            navController.navigate("history")
        }
    )
}

Check for purchases on app launch

The initialization code we added to FocusJournalApplication.kt and ProfileManager.kt handles checking subscription status when the app launches. The ProfileManager automatically updates the premium status based on the user’s access levels.

Here’s how the purchase flow works:

  1. App Launch: refreshProfile() gets the latest subscription status from Adapty
  2. Profile Updates: Purchases automatically update the profile state
  3. Access Control: The isPremium StateFlow controls which features are available
  4. Real-time Updates: Profile changes trigger UI updates throughout the app

Test your integration

Build and run your app:

./gradlew assembleDebug

The expected flow is as follows:

  • Load subscription status from Adapty on launch
  • Show premium features to subscribers
  • Display paywalls to non-subscribers
  • Process purchases through Google Play Billing and unlock content immediately
  • Handle subscription status changes in real-time

Congrats!

At this point, your Android app:

  • Loads paywalls from Adapty
  • Processes subscriptions through Google Play Billing with native Android interfaces
  • Unlocks premium features based on access levels from Adapty
  • Handles subscription status automatically across app launches

Learn more about Android best practices with Adapty in our documentation.

Ben Gohlke
Developer Advocate
General

On this page

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