This post was originally published on androidshin.dev.

When the Play Console notice landed, it landed on ten apps at once. Each one said the same thing: I couldn't submit an update unless it targeted a recent enough Android version, with a hard date of August 31, 2026. Seven of the ten also carried a second notice — they had to move to Google Play Billing Library 8. I had a weekend, a to-do list of ten projects built at different times with different tooling, and a strong suspicion that "just bump one number" was a lie. It was.

This is the field log. Not a tidy tutorial — the actual order I did things in, the errors in the order they appeared, and where I burned time. If you're staring at the same notice, you can skip the traps I walked into.

The version chain nobody mentions up front

The requirement reads as "set targetSdk = 36." What it actually means, once you follow the errors, is closer to a small tooling upgrade per project. Here's the chain I ended up applying to every app, because each link forces the next:

  • targetSdk = 36 requires compileSdk = 36 — you can't target an API you don't compile against.
  • compileSdk = 36 requires Android Gradle Plugin 8.9.0+. Anything older simply doesn't know Android 16 exists. My projects ranged from AGP 8.2.2 to 8.7.3; all of them had to move to 8.9.1.
  • AGP 8.9 requires Gradle 8.11.1+. Every wrapper that was on 8.2, 8.7, or 8.9 had to be bumped, or the build stopped with a plain version-mismatch message.
  • The oldest projects were on Kotlin 1.9.x, which the newer AGP complains about. Moving those to Kotlin 2.0.21 cleared it.
  • And of course the machine needs the Android 16 SDK platform and Build-Tools 36 installed, plus JDK 17 to run AGP 8.x.

The thing that made this tedious rather than hard: my ten projects used three different build styles. Some used Groovy build.gradle with a buildscript classpath. Some used Groovy with the plugins {} block. Two used Kotlin DSL build.gradle.kts, and one of those drove everything through a libs.versions.toml version catalog. The edit was the same idea each time, but where the version string lived moved around constantly.

My rule of thumb after ten of these: do the bumps in dependency order (SDK → AGP → Gradle → Kotlin), sync once after each, and let each error message tell you the next step. Trying to change everything at once just merges five error causes into one confusing stack trace.

The billing change was smaller than I feared

Seven apps needed Play Billing Library 8. I'd blocked out hours for this and it took minutes each, because my integrations were already on the modern ProductDetails API. In v8, exactly one thing broke in each app: the queryProductDetailsAsync callback used to hand back a List<ProductDetails>; now it hands back a QueryProductDetailsResult, and you pull the list off it.

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

Enter fullscreen mode Exit fullscreen mode

Everything else I use — enablePendingPurchases(PendingPurchasesParams…), queryPurchasesAsync(QueryPurchasesParams…), acknowledge, consume, subscription replacement — was untouched. If your billing code predates ProductDetails and still calls querySkuDetailsAsync or the no-argument enablePendingPurchases(), you have real removal work first — those are gone in v8.

The error that wasn't a code error #1: a lint wall on release

This is where the weekend stopped being smooth. One app built fine in debug and then died assembling the release with a fatal lint error I hadn't seen in years:

Error: "pro_active" is translated here but not found in
default locale [ExtraTranslation]
    values-en/strings.xml
2 errors, 0 warnings

Enter fullscreen mode Exit fullscreen mode

Two strings existed in the English translation file but not in the default values/strings.xml. Lint treats that as a crash risk, and release builds run lintVital, which is fatal by default. The strings turned out to be dead leftovers from a "Pro" tier I'd removed long ago. The correct fix wasn't to suppress the check with a baseline, it was to delete the two orphaned strings. I then wrote a quick script to compare every locale's keys against the default across all my apps, so this couldn't ambush me again.

The error that wasn't a code error #2: a file lock on Windows

The one that genuinely made me question my changes turned out to have nothing to do with them:

:app:lintVitalAnalyzeRelease
...androidx.compose.runtime.lint.RuntimeIssueRegistry-....jar:
The process cannot access the file because it is being used
by another process

Enter fullscreen mode Exit fullscreen mode

It looks like a Compose lint failure. It isn't. It's Windows telling you another process is holding a lint-cache .jar — usually a stray Gradle daemon, an antivirus scan, or a file-sync client watching the build folder. The fix: gradlew --stop, clean, and rebuild; if it persists, close the IDE and delete the app/build folder, or reboot. I lost twenty minutes reading Compose lint docs before I actually read the second half of the error message. Read the whole line first.

Edge-to-edge: the visual surprise

Targeting recent Android enforces edge-to-edge — the system draws your content behind the status and navigation bars. On my View-based game screens, toolbars slid under the clock and buttons sat under the navigation pill. The fix is to enable it explicitly and consume the insets as padding:

// once, ideally in a shared BaseActivity
WindowCompat.setDecorFitsSystemWindows(window, false)

ViewCompat.setOnApplyWindowInsetsListener(root) { v, insets ->
    val bars = insets.getInsets(
        WindowInsetsCompat.Type.systemBars()
            or WindowInsetsCompat.Type.displayCutout())
    v.setPadding(bars.left, bars.top, bars.right, bars.bottom)
    insets
}

Enter fullscreen mode Exit fullscreen mode

The Compose screens were mostly fine — Scaffold already applies its insets once you call enableEdgeToEdge(). While I was in the themes, I also deleted android:statusBarColor and android:navigationBarColor, which are deprecated and ignored in edge-to-edge.

The odd one out: a transitive dependency I didn't declare

One app got an "outdated SDK version (androidx.fragment 1.1.0)" notice. I don't use Fragments in that app — it's Compose. The old version was pulled in transitively by a Google library. The fix was to declare a modern version explicitly so it wins resolution:

implementation("androidx.fragment:fragment:1.8.5")

Enter fullscreen mode Exit fullscreen mode

Play Console reports on your resolved dependency graph, not just what you wrote in your build file. Sometimes the thing it flags is three libraries deep.

What I'd do differently

Two lessons stuck. First, the tooling upgrade is the real work, not the target number — if I'd bumped AGP/Gradle/Kotlin across all ten apps as a batch first, then flipped targetSdk, I'd have hit the version-matrix errors once instead of ten times. Second, release lint surfaces years of small debt at once — orphaned translations, deprecated theme attributes, outdated transitive deps. Budget time for that, not just for the migration itself.

Net result: ten apps compiling against API 36, seven on Billing 8, all with bumped version codes and ready to upload. The mandatory part was maybe 30% of the effort. The other 70% was pre-existing debt that the deadline finally forced me to pay down.


Versions, Play requirements, and deadlines change over time — treat the specifics here as a snapshot from my own migration and confirm the current numbers in the Play Console and official docs before you ship. Originally published on androidshin.dev, where I keep a running log of solo Android dev war stories and small tools.