Mastering Google Play In‑App Billing for Consumable Products with Kotlin Coroutines
This article explains how to integrate Google Play's in‑app billing for consumable items on Android, covering terminology, end‑to‑end transaction flow, key implementation steps, Kotlin Coroutine‑based pipeline design, common pitfalls, and a practical checklist for reliable payment processing.
Introduction
China's mobile apps are expanding overseas, and Google Play in‑app billing is essential for transaction‑related apps. Although official docs provide a basic overview, developers often encounter numerous issues during integration. This guide focuses on consumable products and details the critical flow, technical points, and common pitfalls.
Terminology
One‑time vs. Subscription products : One‑time products are purchased once and include consumable (e.g., virtual coins) and non‑consumable (e.g., permanent upgrades). Subscription products renew automatically until cancelled. The article only discusses consumable items.
Consume vs. Acknowledge : Both confirm a purchase, but Acknowledge simply marks the order as non‑refundable and can be called via client API acknowledgePurchase() or Google server API. Consume is specific to consumable items, combines acknowledgment and makes the product purchasable again, and can only be invoked via client API consumeAsync().
Business server vs. Google server : The business server handles the app's own logic, while the Google server refers to Google Play’s billing backend.
Transaction Flow Overview
The high‑level transaction flow is illustrated below:
In practice, network instability, user errors, and other uncertainties can cause order anomalies. A robust design must handle all possible states to avoid over‑charging, under‑charging, or missed deliveries.
Key Process Details
Create Order : Business server creates its own order and maps it to a Google product ID.
Establish Connection : Use BillingClient to connect to Google Play; reconnect if the network drops.
Query Product : Retrieve product details from Google Console to obtain SKU information.
Launch Payment : Call launchBillingFlow() to show Google’s payment UI. Pass a obfuscated business order ID so the server can later correlate Google and business orders.
Confirm Order : After a successful callback, invoke consumeAsync() to acknowledge and enable repurchase; Google refunds unacknowledged orders after three days.
Fulfill Order : Business server grants the consumable (e.g., coins) ensuring idempotent execution.
Compensate Orders : Handle cases where payment succeeds but the user does not receive the item. Two scenarios: (1) Server receives Pub/Sub real‑time notifications and performs compensation; (2) Client proactively checks for unconsumed purchases on app start or purchase page and runs consumeAsync() followed by fulfillment.
Technical Implementation
The transaction is event‑driven, leading to deeply nested callbacks. To improve readability, the flow is refactored into a pipeline using Kotlin Coroutines CallbackFlow.
Logic Encapsulation
Each step is wrapped as a separate Flow module with defined input and output, allowing reuse and clear boundaries. Errors terminate the Flow via close() or cancel().
fun queryPurchasesFlow(client: BillingClient?): Flow<List<Purchase>> =
callbackFlow {
client?.queryPurchasesAsync(
BillingClient.SkuType.INAPP
) { p0, p1 ->
when (p0.responseCode) {
BillingClient.BillingResponseCode.OK -> {
// emit the value to the flow
offer(p1)
}
else -> {
// close the flow
close()
}
}
}
awaitClose {
// log & release resources
}
}Pipeline Assembly
Modules are chained with flatMapConcat to create a linear processing sequence:
startConnectionFlow(client).flatMapConcat {
querySkuDetailFlow(client, request)
}.flatMapConcat {
launchBillingFlow(activity, client, it, request)
}.catch { e ->
e.printStackTrace()
}.collect {
processNext(it)
}Compensation flows can be assembled similarly, reusing the same modules without duplicating code.
Overall Design
The billing component is organized into five layers:
Product Layer : Different checkout UI forms (Web, native).
Interface Layer : Simple APIs for initiating payment and order compensation.
Management Layer : Handles data (orders, products) and connection lifecycle.
Core Layer : Implements business logic such as connection establishment, product retrieval, logging, and monitoring.
Support Layer : Leverages existing app infrastructure.
Pitfalls & Checklist
Empty Product Details
billingClient.querySkuDetailsAsync(params) { p0, p1 ->
when (p0.responseCode) {
BillingClient.BillingResponseCode.OK -> {
// p1 is empty
}
}
}Root causes include:
Publishing an internal test version as required by Google Console.
Temporary delay after product creation; retry after a short interval.
Package name or signing certificate mismatch between the app and Play Console.
"Cannot purchase this item" Error
Possible reasons:
Test account not added to the internal test or license‑test list.
Test account has not accepted the invitation link.
Essential Preconditions
Device must have Google Services Framework installed.
Play Store region should not be Mainland China (or set a different country in settings).
Test account added to both internal‑test and license‑test lists.
Accept the test invitation.
Publish an internal test build (no review needed).
Package name and signing certificate must match those uploaded to Google.
Play Store app must be up‑to‑date.
Conclusion
Accurate payment handling is critical for revenue and user trust. Developers need a clear mental model of the end‑to‑end flow and should abstract the process into reusable, linear pipelines using Kotlin Coroutines. While the presented solution covers many scenarios, real‑world edge cases still require automated monitoring, alerting, and continuous improvement.
References
Google Play’s billing system overview
Integrate the Google Play Billing Library into your app
play‑billing‑samples (GitHub)
Is it possible to acknowledge consumable products from server side?
Kotlin flows on Android – Convert callback‑based APIs to flows
Simplifying APIs with coroutines and Flow
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
