In-App Purchase in Android: Google Play Billing Library Guide
Monetizing Android apps through in-app purchases requires integrating Google Play Billing Library. This guide covers setup, product queries, purchase flows, and best practices.
BillingClient Setup
Initialize BillingClient in your Activity or ViewModel:
import com.android.billingclient.api.*
class PurchaseViewModel : ViewModel() {
private lateinit var billingClient: BillingClient
fun initBillingClient(context: Context) {
billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// Ready to query products
queryProducts()
}
}
override fun onBillingServiceDisconnected() {
// Try to reconnect
}
})
}
private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
purchases?.forEach { purchase ->
handlePurchase(purchase)
}
}
BillingClient.BillingResponseCode.USER_CANCELED -> {
// User canceled
}
}
}
}
Query Products
Fetch available products (one-time and subscriptions):
fun queryProducts() {
// One-time products
val oneTimeProductList = listOf(
QueryProductDetailsParams.Product(
productId = "premium_unlock",
productType = BillingClient.ProductType.INAPP
)
)
val params = QueryProductDetailsParams.newBuilder()
.setProductList(oneTimeProductList)
.build()
billingClient.queryProductDetailsAsync(params) { billingResult, products ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
products?.forEach { product ->
Log.d("Billing", "${product.name}: ${product.oneTimePurchaseOfferDetails?.formattedPrice}")
}
}
}
}
Launch Billing Flow
Trigger the purchase flow when user clicks "Buy":
fun launchPurchaseFlow(activity: Activity, productDetails: ProductDetails) {
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()
)
)
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
}
Acknowledge Purchases (Required within 3 days)
After a successful purchase, acknowledge it within 3 days to prevent refund:
private fun handlePurchase(purchase: Purchase) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d("Billing", "Purchase acknowledged")
unlockPremiumFeatures()
}
}
}
}
Subscriptions with OfferToken
Handle subscription products with offer selection:
fun querySubscriptions() {
val subscriptionProductList = listOf(
QueryProductDetailsParams.Product(
productId = "premium_monthly",
productType = BillingClient.ProductType.SUBS
)
)
val params = QueryProductDetailsParams.newBuilder()
.setProductList(subscriptionProductList)
.build()
billingClient.queryProductDetailsAsync(params) { billingResult, products ->
products?.forEach { subscription ->
subscription.subscriptionOfferDetails?.forEach { offer ->
Log.d("Billing", "Offer: ${offer.basePlanId}, Price: ${offer.pricingPhases}")
}
}
}
}
fun launchSubscriptionFlow(activity: Activity, productDetails: ProductDetails, offerToken: String) {
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
)
)
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
}
Purchase Restoration
Restore previous purchases on app reinstall:
fun restorePurchases() {
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
) { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
purchases.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
unlockPremiumFeatures()
}
}
}
}
}
Compose Purchase UI
Build a purchase UI in Compose that lists products:
@Composable
fun PremiumShop(viewModel: PurchaseViewModel) {
val products by viewModel.products.collectAsState()
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(products) { product ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable {
viewModel.launchPurchase(product)
}
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = product.name,
style = MaterialTheme.typography.titleMedium
)
Text(
text = product.description ?: "",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { viewModel.launchPurchase(product) },
modifier = Modifier.align(Alignment.End)
) {
Text("Buy for ${product.oneTimePurchaseOfferDetails?.formattedPrice ?: "?"}")
}
}
}
}
}
}
Best Practices
- Acknowledge within 3 days: Google Play can refund unacknowledged purchases after 3 days
-
Test with Test Products: Use Google Play Console's test product IDs (e.g.,
android.test.purchased) - Handle Network Errors: Gracefully retry on connection failures
- Show Purchase Receipts: Always inform users of successful purchases
- Restore Purchases: Allow users to restore purchases across devices
- Encrypt User Data: Never store purchase tokens in plaintext locally
Key Takeaways
- BillingClient is the official Google library for all purchase flows
- Three-day acknowledgement requirement is critical to prevent automatic refunds
- OfferToken enables flexible subscription pricing with trials and base plans
- Purchase restoration provides seamless user experience across reinstalls
- Compose makes it easy to build intuitive purchase UIs with reactive state management
${ CTA }
Top comments (0)