Android In-App Updates — Common pitfalls and some good patterns

Dhruv Taneja
ProAndroidDev
Published in
8 min readJun 5, 2019
Photo by Nielsen Ramon on Unsplash

Google released the In-app updates feature in which the apps can nudge the users to update their apps without even going to the play store. If an update is available, the users will see a dialog or a full blocking screen where the UI is generated and controlled by google play.

In-app updates can be offered with a couple of different UX. In case of a non-blocking dialog, it will look something like this -

source: https://developer.android.com/guide/app-bundle/in-app-updates

This one is called Flexible in-app update. Good if you want the user to use the app while the update is being installed. Go for this one if you don’t want to deter the user from transacting in your app.

And in case of a full screen blocking UI, something like this will come up

source: https://developer.android.com/guide/app-bundle/in-app-updates

And this one is Immediate in-app update flow. Good when the update is usage critical. Go for this one if the update is necessary for a better transaction experience.

Implementation

This article is not much about the API details. But allow me to spend some time showing the implementation in brief, to lay the grounds to discuss those common pitfalls and those good patterns. Those who are well versed with that, feel free to skip this part.

To have this in-app update feature in your app, you need to check 2 things on the list —
1. Minimum API should be 21 (Lollipop 5.0)
2. Add Play Core Library in your app dependency

Dependency for integrating in-app updates

The rest of the integration is pretty simple. Let’s say you want to tell the user about an available app update when the user launches your app. So in the onCreate of your launcher or your main activity, you would request app update availability info from Play Core. The class AppUpdateManager of the Play Core library is used to get the AppUpdateInfo object. That object has info about update availability, available version, and some update statuses.

Since fetching the update info is asynchronous, you would need a callback like this to get the update.

The AppUpdateInfo object also has an intent that you can fire up to start the request flow. Based on the type of update flow you want to choose for the user(flexible, or immediate), start the update flow.

Starting app request flow by calling appUpdateManager.startUpdateFlowForResult()

The callback of the user’s action(accepting the update, or denying it) will be received in onActivityResult of the activity instance passed above. It can be one of these: Activity.RESULT_OK, Activity.RESULT_CANCELED, ActivityResult.RESULT_IN_APP_UPDATE_FAILED. The last one comes from the play core library and happens when the update failed for some reason.

Handling user’s action in onActivityResult

Now the remaining of your activity’s code will differ depending upon the type of update flow you choose, i.e., AppUpdateType.FLEXIBLE or AppUpdateType.IMMEDIATE(Of course, you can choose to have both implementations in your app at the same time too).

Immediate Update

Ideally, during an immediate update, the user should be restricted from using your app. Otherwise, the app will auto restart, probably unfortunately when the user is in the middle of doing something in your app. It’s the same cringe feeling you get when the Play Store decides to restart an app after installing, while you’re still using it.

After calling appUpdateManager.startUpdateFlowForResult() for an immediate update(as shown above), the play store takes control very nicely. The user will see the update progress till the time he/she is on the new version. And if in any scenario the user goes back or kills the app, or gets a call and the app goes into the background, it won’t stop the update process. The same, therefore, should be communicated to the user the moment the app gets back to the foreground. And the best place to handle that would be your activity’s onResume.

The integer UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS means that an immediate update has been started, and update is still in progress. Triggering the request flow using update info’s intent will ask Google Play to show that blocking, immediate app update screen.
Post the update, Play will automatically restart the app.

Flexible Update

Once the users accept the app update in the flexible update flow, they are free to search recipes, like photos, edit their selfies, or do whatever that your app lets them do. The only way of update visible to the users would be a download gif in their notification tray.
But you, the developer can hook up to more updates while all that is going on. An InstallStateUpdatedListener can be registered with the AppUpdateManager, and it will keep you posted about the install status(or the error). Based on that, the UI can react and give a nudge to the user to restart the app to install the update.

InstallStatus.DOWNLOADED means that the download is complete and it is time to let the users know about it. There are a few ways to do that, showing a snackbar, a dialog, or maybe a notification.
To complete the update, appUpdateManager.completeUpdate() must be called, after which you should unregister the listener. That will start the Google Play’s install screen with a nice to look at the animation loop(the lengths to which Google goes for the users to make it worth their time ❤️).

If appUpdateManager.completeUpdate() is done in the background(from a notification action perhaps), Play’s install screen won’t show up.

Just like in an immediate update, leaving the app won’t stop the update during a flexible update as well. So it is important to let your users know about the update status when they return to your app. Again, the onResume of your activity is a good place to do this.

In your activity’s onResume, get the update info to investigate the install status. InstallStatus.DOWNLOADED means the update has been successfully downloaded. And it is time to tell the user about the same, just like before.

For those who are looking at a much better explanation of the how-to, check out this article by Joe Birch. Also, the official documentation is really helpful and comprehensive.

Pro Tip: While you are integrating this feature for your app, I would advise you to create another tiny app just for this purpose. That will help you figure out all the required code changes, and not disturbing and flooding your testing or production tracks of your main apps at the same time.
I created Flexi Update to iterate and research in-app updates. Trust me, publishing the new app would take a couple of hours, but it will save you a lot of time as you won’t be building a release bundle of your possibly gigantic production app. Also, your release tracks stay clean.

Common pitfalls and some good practices

While playing around with in-app updates, I found myself staring at the code and even cursing myself sometimes because of some corner cases(and some silly mistakes). With the hope of saving your time while you integrate in-app updates in your app(and without the fear of being judged for my naive mistakes), I’ll mention a few gotchas, dos and don’ts —

Testing this won’t work on a debug build.

You would need a release build signed with the same key you use to sign your app before uploading to the Play Store. It would be a good time to use the internal testing track.

The account being used for testing should have downloaded the app once at least.

This obviously means that a release build has to be installed first from the Play Store, and not from adb using a bundle tool.

In case of no internet, the Play Core Library says no update available.

If your app decides to configure the UI based on app update availability, it should also handle no internet conditions. In case of no internet, appUpdateInfo.updateAvailability() returns UpdateAvailability.UPDATE_NOT_AVAILABLE. And appUpdateInfo.isUpdateTypeAllowed will always return false without the internet.

Use LiveData to avoid leaking memory and crashes, everything is async here.

Calling getAppUpdateInfo() returns an object of Task<AppUpdateInfo> which is used for async programming. Adding UI changes in the listener might not be a great idea, because the activity is not guaranteed to be in the suitable lifecycle state to allow handling UI. So it is best to update values on a LiveData object and observe it to trigger UI changes.

The UI can be updated once the live data’s value changes. The lifecycle of your activity won’t have to care about completion of that Taskat all.

Choose the update notification channel for a flexible update that is most appropriate for your app.

For some apps, notifications might work really well for interacting with users. For some, users are likely to disable notification channels, and some might have high user interaction with in-app popups. For information as critical as a successful or a failed update, choose what works best for your app.

Give your users more than one ways to accept the update.

You should respect your users’ decisions. If they do not want to engage positively with your in-app update dialog, do not put it on their faces every time they open the app. At least till the time they haven’t installed another update via Play Store. Meanwhile, it is better to give them another entry point to download the update. Maybe, you can show a notification dot somewhere in your bottom navigation. Or on your hamburger, or on the bell icon you have in your app. And from that entry point, you can bring up the in-app update dialog. Showing it all the time to the users who don’t really want to update will likely do the opposite of what you really want from it.

Log. Log. Log. Log everything.

You can consider this one as one of the most important patterns. Logging really helps when there aren’t any major UI clues about what’s going on under the hood. Since most of your testing is gonna happen in release builds, consider logging important information in toast messages. If the short-lived nature of toast messages is not being of any help, appending logs in a TextView in your activity is also a good option. Just log anything that you think will help you debug. Believe me, it will save you from uploading countless unnecessary builds just because you don’t what happened in the Play Core API callbacks. Log as if your code is reading its script to you.

Pro tip: If you are stripping android.util.Log code calls using proguard, turn that off and (smartly) spam your logcat. Don’t forget to revert it for production though.

Conclusion

In this article, we saw what it takes to integrate the in-app updates feature. And then we saw some details on some good ways to do it faster.

Integrating in-app updates is simple. But testing it can be irksome and sometimes time-consuming. I hope this article puts out a few ways to save you time and lets you focus more on getting those important releases out. Let me know how it affected your app adoption and business in your comments. And do share your own good patterns and challenges you faced while integrating this feature.

Also, don’t forget to CLAP if you found this useful.

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Responses (26)

What are your thoughts?

My appupdateinfo.updateavailability() always returns 1 (UPDATE_NOT_AVAILABLE) and I am running on a release version lower than current in play store. I read thru the pitfalls and made sure I did not fall in that category.
What should be my next steps to make sure that conditional statement is true?

AppUpdateType.IMMEDIATE and AppUpdateType.FLEXIBLE values can be picked up from API or remote config to have the flexibility on API deprecation or feature upgradation starting from certain versions after a certain period.
Dhruv Taneja your thoughts on this as a best practice?

I am getting an error Install Error(-3): The API is not available on this device.
so how can I solve it?