Mr.AndroidShin / Dev Tools

Debugging log · Billing

Play Billing Library 8: the one change that touched every app

Alongside the Android 16 deadline, Google Play also requires apps to use Play Billing Library 8 or higher for updates. I braced for a painful rewrite across seven apps with in-app purchases. It turned out to be far smaller than expected — in most projects a single callback and a version number. Here's the exact change so you can do it in minutes, not hours.

The short version: the breaking change that matters is queryProductDetailsAsync. Its callback used to hand you a List<ProductDetails>; in v8 it hands you a QueryProductDetailsResult, and you pull the list off it with getProductDetailsList(). Bump the dependency, fix that one call, and most integrations are done.

Bump the dependency

// was 7.x.x
implementation("com.android.billingclient:billing:8.0.0")
// or the KTX artifact
implementation("com.android.billingclient:billing-ktx:8.0.0")

After the deadline you can't publish updates built against v7, so this is not optional if you plan to ship again.

The callback change (Java)

Before — the second parameter was the list itself:

billingClient.queryProductDetailsAsync(params, (billingResult, list) -> {
    if (billingResult.getResponseCode() == BillingResponseCode.OK && list != null) {
        for (ProductDetails pd : list) { /* ... */ }
    }
});

After — the second parameter is a result object; get the list from it:

billingClient.queryProductDetailsAsync(params, (billingResult, result) -> {
    List<ProductDetails> list =
        (result == null) ? null : result.getProductDetailsList();
    if (billingResult.getResponseCode() == BillingResponseCode.OK && list != null) {
        for (ProductDetails pd : list) { /* ... */ }
    }
});

The callback change (Kotlin)

// before
client.queryProductDetailsAsync(params) { _, list ->
    list.forEach { /* ... */ }
}
// after
client.queryProductDetailsAsync(params) { _, result ->
    result.productDetailsList.forEach { /* ... */ }
}

That's the whole change in the common case. The QueryProductDetailsResult object also exposes an unfetched-products list if you want to detect product IDs that came back empty, but you don't need it to compile.

What you probably do NOT need to change

This is where the time savings came from. If your integration is already modern (v6/v7 style), these were untouched in every app I migrated:

The removed APIs (only if you still use them)

If your code is older, watch for these — they're gone in v8 and need replacing first: querySkuDetailsAsync (use queryProductDetailsAsync), queryPurchaseHistoryAsync, and the no-arg enablePendingPurchases(). If you're already on ProductDetails, you almost certainly don't touch any of these.

Billing is real money, so build a release and actually run a purchase and a restore on a device before you ship — a subscription upgrade, a consumable, and a restore-after-reinstall. A compile-clean migration is not the same as a tested one.

Library versions and Play requirements shift; confirm the current minimum billing version in the Play Console before uploading.