The Android In-App Update API in 2026: flexible vs. immediate, and when to force one
A practical guide to Google's In-App Update API — flexible vs. immediate flows, update priority, staleness days, and the resume check everyone forgets to write.
Play Store auto-update covers most of your users eventually, but “eventually” is doing a lot of work in that sentence. Wi-Fi-only settings, storage pressure, or a user who just never opens the Play Store app can leave someone three versions behind — still running the build with the bug you fixed two weeks ago, or worse, still hitting a server-side contract your backend stopped supporting. Google’s In-App Update API exists for that gap: it lets your app check for a newer version and prompt the update from inside itself, without sending the user anywhere.
The API has two distinct flows, and picking the wrong one is the most common way people ship this badly.
How it actually works
The API lives in Play Core (com.google.android.play:app-update-ktx). You ask AppUpdateManager for the current AppUpdateInfo, which tells you whether an update is available and what Google knows about it:
val updateManager = AppUpdateManagerFactory.create(context)
updateManager.appUpdateInfo.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
// decide which flow to start based on info.updatePriority()
// and info.clientVersionStalenessDays()
}
}
That AppUpdateInfo object is the whole API, really — everything else is deciding what to do with it.
Flexible vs. immediate: two different jobs
Flexible downloads the update in the background while the user keeps using the app, then shows a small “restart to update” snackbar when it’s ready. The user isn’t blocked and can dismiss the prompt. This is the right default for anything that isn’t urgent — a UI polish pass, a new feature, a non-breaking bug fix.
if (info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
updateManager.startUpdateFlowForResult(
info, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()
)
}
Once the download finishes, InstallStateUpdatedListener reports InstallStatus.DOWNLOADED, and you complete the install explicitly:
updateManager.registerListener { state ->
if (state.installStatus() == InstallStatus.DOWNLOADED) {
updateManager.completeUpdate() // shows the snackbar's restart action
}
}
Immediate is the opposite: a full-screen, blocking flow that stops the user from using the app until the update installs. It’s disruptive by design, which means it’s for exactly one situation — the current version is broken in a way that matters. A security fix, a Room schema migration that an old client would corrupt, a backend API change that makes the old build crash on every request. If you can’t articulate why letting someone keep using the old version for another day is actually harmful, it’s not an immediate update, it’s a flexible one.
if (info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
updateManager.startUpdateFlowForResult(
info, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}
I’ve only reached for immediate once, shipping a Granyn release that changed how recurring transactions were stored — an old client writing to the new schema would have silently miscalculated balances. That’s worth blocking on. A renamed button is not.
Deciding priority (and staleness)
AppUpdateInfo gives you two signals instead of leaving the flexible/immediate choice to guesswork:
updatePriority()— an integer 0–5 that you set yourself, per release, in the Play Console at rollout time. It’s not computed from the diff; you’re telling Google (and your own client code) how urgent this release is.clientVersionStalenessDays()— how many days it’s been since the update became available to this specific user, which is different from how long ago you shipped it. A staggered rollout means this number varies across your install base even for the same release.
A pattern that’s held up well for me: treat priority 4–5 as immediate-eligible, and combine priority with staleness for everything else — a priority-3 release becomes immediate only after it’s sat unclaimed for a week or two, so users get a grace period before you interrupt them.
val shouldForce = info.updatePriority() >= 4 ||
(info.updatePriority() == 3 && (info.clientVersionStalenessDays() ?: 0) > 14)
The check everyone forgets: resuming a stalled update
An immediate update can be interrupted — the user backgrounds the app, the process dies, a phone call comes in. When that happens, Play Core doesn’t silently resume it for you; you have to check for a stalled update every time the relevant screen resumes, not just once on launch:
override fun onResume() {
super.onResume()
updateManager.appUpdateInfo.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
updateManager.startUpdateFlowForResult(
info, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}
}
}
Skip this and you get the failure mode that gives the API a bad reputation: a user gets interrupted mid-update, backgrounds the app to answer a text, comes back, and the app just… works again, half-updated, in a state you never tested. The onResume check is three lines and it’s the difference between a robust immediate update and an intermittent bug report you can’t reproduce.
Testing without waiting on a real rollout
You can’t trigger a real update from your own device against your own currently-installed build — Play Core needs an actual version difference visible to your account. The practical setup is an internal testing track: install an older version code from it, then push a newer one and let the API see the real gap. Google also ships an in-app update testing API for faking AppUpdateInfo responses locally, which is worth wiring into a debug menu if you’re touching this flow often — it’s much faster than a real track round-trip for iterating on the priority/staleness logic itself.
Treat the whole feature as a safety net, not a UX pattern to lean on. The best version of this API is the one your users never see, because auto-update already did its job — flexible and immediate exist for the days it didn’t.
// Related reading
More from the journal
androidx.startup in 2026: ordering library initialization without a stack of ContentProviders
A practical guide to Android's App Startup library — consolidating initializers into one ContentProvider, declaring dependencies, and the lazy-init cases it can't replace.
CameraX in 2026: binding Preview and ImageAnalysis without leaking a camera session
A practical guide to CameraX on Android: binding Preview and ImageAnalysis to the lifecycle, the right backpressure strategy, and the crash rotation causes.
The Android Photo Picker API in 2026: letting users attach a photo without granting gallery access
A practical guide to Android's Photo Picker API — single and multi-select, MIME filtering, the pre-Android-13 backport, and why it beats READ_MEDIA_IMAGES for privacy and approval.