SuperAPI AI Directory

integrating-pbl

General & other

View repository

Updated 10-16 07:58

About this skill

Integrating the Play Billing Library

You are wiring the Play Billing Library into an Android app. This skill covers the client side setup: adding the dependency, building BillingClient, managing the connection, and querying products. Follow the phases in order.

Phase 0: Intent

Confirm what the caller actually wants before you touch code. Pick one:

Intent Signal
First time integration No com.android.billingclient dependency yet
Upgrading PBL Existing BillingClient on an older version
Fixing connection bugs SERVICE_DISCONNECTED, missing onBillingSetupFinished callbacks
Adding product queries BillingClient exists, no queryProductDetailsAsync call

If the request is ambiguous, ask. Do not write Gradle edits or Kotlin until you know the target state.

Phase 1: Discovery

Inspect the project before changing anything.

Check the Gradle setup:

grep -r "com.android.billingclient" --include="*.gradle*"

Check existing BillingClient usage:

grep -rn "BillingClient" --include="*.kt" --include="*.java" src/

Record these facts:

Fact Why it matters
Current PBL version Drives migration steps (PBL 8 and 9 require PendingPurchasesParams)
Context passed to builder Activity contexts leak; prefer applicationContext
Whether enablePendingPurchases is called Missing call throws IllegalStateException on build()
Whether auto reconnection is enabled Absent means you own retry logic
Where BillingClient is instantiated ViewModel, singleton, or Application scoped
Whether KTX is on the classpath Decides callback vs coroutine style

If you find multiple BillingClient instances in one process, flag it. One instance per process is the rule.

Phase 2: Plan

Before writing code, pick a connection strategy and confirm with the caller.

Strategy Use when Trade off
Application scoped singleton Billing gates many features, frequent status checks Connection stays open while idle
ViewModel / feature scoped Billing only appears on one or two screens Pays setup cost every entry

Then decide:

  1. Callback style or KTX coroutine style. If coroutines are already the norm in the project, pick KTX.
  2. Auto reconnection on or off. Default to on unless you need manual control.
  3. Which product IDs and types (INAPP, SUBS) the initial query covers. Batch up to 20 per call.

Write the plan down. One BillingClient owner, one listener, one connection strategy.

Phase 3: Execute

Step 1 Add the dependency

In the module build.gradle.kts:

dependencies {
    implementation("com.android.billingclient:billing-ktx:9.0.0")
}

The billing-ktx artifact pulls in the base library plus coroutine extensions. Drop -ktx if you do not want KTX.

Step 2 Build the BillingClient

Use applicationContext to avoid activity leaks:

val billingClient = BillingClient.newBuilder(appContext)
    .setListener(purchasesUpdatedListener)
    .enablePendingPurchases(
        PendingPurchasesParams.newBuilder().build()
    )
    .enableAutoServiceReconnection()
    .build()

All four calls are required for a working PBL 9 setup. Missing setListener throws IllegalArgumentException. Missing enablePendingPurchases throws IllegalStateException.

Step 3 Wire the PurchasesUpdatedListener

One listener per client. Dispatch from it if other components need to react:

val purchasesUpdatedListener =
    PurchasesUpdatedListener { result, purchases ->
        when (result.responseCode) {
            BillingResponseCode.OK ->
                purchases?.forEach(::handlePurchase)
            BillingResponseCode.USER_CANCELED -> Unit
            else -> logError(result)
        }
    }

The callback fires on the main thread and can fire while the app is in the background. Update a repository or state holder, not UI directly. Never force unwrap purchases; it is null on non OK responses.

Step 4 Start the connection

Callback form:

billingClient.startConnection(object : BillingClientStateListener {
    override fun onBillingSetupFinished(result: BillingResult) {
        if (result.responseCode == BillingResponseCode.OK) {
            onBillingReady()
        }
    }
    override fun onBillingServiceDisconnected() { /* auto retry */ }
})

KTX form:

val result = billingClient.startConnection()
if (result.responseCode == BillingResponseCode.OK) {
    onBillingReady()
}

Do not call billing methods before onBillingSetupFinished fires with OK. Queue pending work and flush on each successful connect.

Step 5 Query product details

Build the product list, mix INAPP and SUBS in one call, keep each batch at or under 20 products:

val products = listOf(
    QueryProductDetailsParams.Product.newBuilder()
        .setProductId("premium_monthly")
        .setProductType(ProductType.SUBS)
        .build()
)
val params = QueryProductDetailsParams.newBuilder()
    .setProductList(products)
    .build()

Callback form:

billingClient.queryProductDetailsAsync(params) { result, list ->
    if (result.responseCode == BillingResponseCode.OK) {
        displayProducts(list)
    }
}

KTX form:

val result = billingClient.queryProductDetails(params)
val details = result.productDetailsList

Do not cache ProductDetails across sessions. Prices can change; re query each time you display products.

Step 6 Handle UnfetchedProduct results

PBL 8 and 9 return a QueryProductDetailsResult that may include UnfetchedProduct entries. Branch on the per product response code:

Code Action
SERVICE_UNAVAILABLE Retry with backoff
SERVICE_TIMEOUT Retry with backoff
ERROR Retry, then investigate Play Console
DEVELOPER_ERROR Do not retry; fix product ID or type

Cap retries at two or three attempts. Show the products you did fetch and log the rest.

Step 7 Handle disconnection

With enableAutoServiceReconnection(), PBL retries for you using exponential backoff. Your job is only to retry the in flight operation that failed with SERVICE_DISCONNECTED. If you disabled auto reconnection, implement backoff yourself starting at one second and capping near fifteen seconds.

Check billingClient.isReady before issuing a call if you need a fast path. Always call endConnection() when the owning scope is destroyed.

Phase 4: Verify

Before you mark the integration done, confirm each of these:

Check How
Dependency present ./gradlew :app:dependencies lists billing-ktx at the target version
Single client per process grep -rn "BillingClient.newBuilder" src/ returns one call site
enablePendingPurchases called Build passes without IllegalStateException at first build()
Listener fires on purchase Run a test purchase on the internal track; confirm OK branch hit
Connection ready before calls Log onBillingSetupFinished and confirm it precedes queries
Product query returns details Log productDetailsList.size for a known batch
Unfetched handling works Query a deliberately wrong product ID and confirm you do not retry DEVELOPER_ERROR
Disconnect recovery Toggle airplane mode; confirm auto reconnect and that queued work runs
endConnection() on teardown Log it in the owning scope's destroy path

If any row fails, return to Phase 2 and revise the plan. Do not paper over a failing check.

References

  • Full chapter

  • Google Play Billing Library release notes and migration guides for version specific details.