In-App Updates (Java)
RecipeBox ships new features fairly often — a better shopping-list generator, a redesigned recipe editor, occasionally a security patch. Most users never open the Play Store to check for updates, so a stale copy of the app can sit on a device for months. The In-App Updates API, part of Google's Play Core library, lets RecipeBox ask the user to update without leaving the app or being sent to the Play Store manually. The prompt appears right inside the app, the update downloads through Play, and the user is back in RecipeBox in seconds.
This is a Play Store distribution feature — it only works for apps installed through Google Play, since it talks to the Play Store app in the background to check for and fetch updates.
Two Update Flows
The Play Core library gives you exactly two ways to prompt for an update, and the choice matters — picking the wrong one either annoys users or leaves them running risky old code.
| Flow | Behavior | Good for |
|---|---|---|
| FLEXIBLE | Downloads the update in the background while the user keeps using RecipeBox normally; a small snackbar asks them to restart once the download finishes. | Routine feature releases, minor fixes, general polish. |
| IMMEDIATE | A full-screen, blocking UI takes over the app until the update is downloaded and installed. The user cannot dismiss it and keep using the old version. | Genuinely critical fixes — e.g. a security vulnerability or a bug that corrupts saved recipes. |
App startup
Check for update
Update available?
Read priority/severity
Choose flow
FLEXIBLE or IMMEDIATE
Install
Restart RecipeBox on new version
Adding the Dependency
// app/build.gradle
dependencies {
implementation 'com.google.android.play:app-update:2.1.0'
}
Checking for an Available Update
AppUpdateManager is the entry point. Its getAppUpdateInfo() call returns a task describing whether an update exists, and — importantly — whether that particular update flow type is even allowed for it.
// In RecipeBox's MainActivity.java
private AppUpdateManager appUpdateManager;
private static final int UPDATE_REQUEST_CODE = 5001;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
appUpdateManager = AppUpdateManagerFactory.create(this);
checkForUpdate();
}
private void checkForUpdate() {
Task<AppUpdateInfo> appUpdateInfoTask = appUpdateManager.getAppUpdateInfo();
appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
boolean updateAvailable =
appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE;
boolean flexAllowed =
appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE);
if (updateAvailable && flexAllowed) {
startFlexibleUpdate(appUpdateInfo);
}
});
}
- getAppUpdateInfo() talks to the Play Store app, not a network API you host yourself.
- isUpdateTypeAllowed() guards against requesting a flow the current update doesn't support.
Starting a Flexible Update
// Still inside MainActivity.java
private void startFlexibleUpdate(AppUpdateInfo appUpdateInfo) {
try {
appUpdateManager.startUpdateFlowForResult(
appUpdateInfo,
AppUpdateType.FLEXIBLE,
this,
UPDATE_REQUEST_CODE
);
} catch (IntentSender.SendIntentException e) {
Log.e("RecipeBoxUpdate", "Unable to start update flow", e);
}
}
- The this and request code let RecipeBox receive the result in onActivityResult() if the user cancels the download prompt.
- For FLEXIBLE, the user keeps using RecipeBox right away — the download happens quietly behind the scenes.
Tracking Download Progress
Because a flexible update downloads in the background, RecipeBox should register an InstallStateUpdatedListener so it knows exactly when the download is finished and it's time to ask the user to restart.
// A listener kept as a field so it can be unregistered later
private final InstallStateUpdatedListener listener = state -> {
if (state.installStatus() == InstallStatus.DOWNLOADED) {
showRestartSnackbar();
}
};
@Override
protected void onResume() {
super.onResume();
appUpdateManager.registerListener(listener);
}
@Override
protected void onPause() {
appUpdateManager.unregisterListener(listener);
super.onPause();
}
private void showRestartSnackbar() {
Snackbar snackbar = Snackbar.make(
findViewById(R.id.rootLayout),
"A new version of RecipeBox is ready",
Snackbar.LENGTH_INDEFINITE);
snackbar.setAction("RESTART", v -> appUpdateManager.completeUpdate());
snackbar.show();
}
Completing the Install
appUpdateManager.completeUpdate() installs the already-downloaded update and restarts RecipeBox on the new version. For an IMMEDIATE flow this step happens automatically as part of the blocking UI; for FLEXIBLE, it only runs when the user taps that restart action, since silently killing and relaunching the app mid-session would be jarring.
Choosing Between the Two Flows Honestly
It's tempting to reach for IMMEDIATE every time, since it guarantees everyone ends up on the latest build. In practice that's a bad habit — a full-screen forced update for a minor RecipeBox change (say, a new recipe category color) is a genuinely annoying interruption for someone who just wants to look up a recipe. Reserve IMMEDIATE for updates that fix something that actually puts the user at risk: a security patch, a data-loss bug, or a break in core functionality that FLEXIBLE's "update whenever you're ready" pacing can't safely wait out. Everything else — new features, UI polish, small bug fixes — belongs in FLEXIBLE.