keep your app users up to date on their devices
Users may test out new features, have access to speed enhancements, and take advantage of bug fixes when they keep your software updated on their devices. Although some users choose to enable background updates when using an unmetered connection, other users might need a reminder to do so.
Active users are prompted to upgrade your app using the in-app updates functionality in Google Play Core libraries.
Devices running Android 5.0 (API level 21) or higher are compatible with the in-app updates feature.
Here are two methods for displaying updates within your app:
Flexible
If the user wants to update the app, a popup window will ask them. Both acceptance and denial are options. If they agree, the update will start downloading behind the scenes. When your update offers a few modest UI tweaks or performance upgrades, utilize this.
Photo on In-app updates
Immediate
The user must update the app in order to use this full-screen UX indefinitely. You can utilize this if you have a crucial update, such as a security patch.
Photo on In-app updates
There are two signals that can start the update:
- Priority: You specify the update’s importance in each release by providing an integer that ranges from 0 to 5. (5 being the highest priority). In order to update the app, this will start the appropriate update flow (Immediate or Flexible).
- Staleness: Specifies the amount of time the device has been aware that an update is available. This aids in setting off the appropriate flow. For instance, the Flexible flow would be triggered if the user hadn’t updated the app in the previous 30 days following the release of the update, and the Immediate flow would be triggered if it had been longer than 90 days.
For a better user experience, you may also combine the two signals.
How to implement in-app updates in Android
Add the following dependencies to your module-level gradle.build file.
dependencies { | |
implementation 'com.google.android.play:core:1.7.0' | |
} |
We’re going to put everything we need in a separate file to make implementation simpler, and then we’re going to call it from the Activity we want to check for updates.
The following code should be pasted into a new file called InAppUpdate.kt
import android.app.Activity | |
import android.content.Intent | |
import android.graphics.Color | |
import android.util.Log | |
import androidx.appcompat.app.AppCompatActivity | |
import com.google.android.material.snackbar.Snackbar | |
import com.google.android.play.core.appupdate.AppUpdateInfo | |
import com.google.android.play.core.appupdate.AppUpdateManager | |
import com.google.android.play.core.appupdate.AppUpdateManagerFactory | |
import com.google.android.play.core.install.InstallState | |
import com.google.android.play.core.install.InstallStateUpdatedListener | |
import com.google.android.play.core.install.model.AppUpdateType | |
import com.google.android.play.core.install.model.InstallStatus | |
import com.google.android.play.core.install.model.UpdateAvailability | |
class InAppUpdate(activity: Activity) : InstallStateUpdatedListener { | |
private var appUpdateManager: AppUpdateManager | |
private val MY_REQUEST_CODE = 500 | |
private var parentActivity: Activity = activity | |
private var currentType = AppUpdateType.FLEXIBLE | |
init { | |
appUpdateManager = AppUpdateManagerFactory.create(parentActivity) | |
appUpdateManager.appUpdateInfo.addOnSuccessListener { info -> | |
// Check if update is available | |
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) { // UPDATE IS AVAILABLE | |
if (info.updatePriority() == 5) { // Priority: 5 (Immediate update flow) | |
if (info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) { | |
startUpdate(info, AppUpdateType.IMMEDIATE) | |
} | |
} else if (info.updatePriority() == 4) { // Priority: 4 | |
val clientVersionStalenessDays = info.clientVersionStalenessDays() | |
if (clientVersionStalenessDays != null && clientVersionStalenessDays >= 5 && info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) { | |
// Trigger IMMEDIATE flow | |
startUpdate(info, AppUpdateType.IMMEDIATE) | |
} else if (clientVersionStalenessDays != null && clientVersionStalenessDays >= 3 && info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { | |
// Trigger FLEXIBLE flow | |
startUpdate(info, AppUpdateType.FLEXIBLE) | |
} | |
} else if (info.updatePriority() == 3) { // Priority: 3 | |
val clientVersionStalenessDays = info.clientVersionStalenessDays() | |
if (clientVersionStalenessDays != null && clientVersionStalenessDays >= 30 && info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) { | |
// Trigger IMMEDIATE flow | |
startUpdate(info, AppUpdateType.IMMEDIATE) | |
} else if (clientVersionStalenessDays != null && clientVersionStalenessDays >= 15 && info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { | |
// Trigger FLEXIBLE flow | |
startUpdate(info, AppUpdateType.FLEXIBLE) | |
} | |
} else if (info.updatePriority() == 2) { // Priority: 2 | |
val clientVersionStalenessDays = info.clientVersionStalenessDays() | |
if (clientVersionStalenessDays != null && clientVersionStalenessDays >= 90 && info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) { | |
// Trigger IMMEDIATE flow | |
startUpdate(info, AppUpdateType.IMMEDIATE) | |
} else if (clientVersionStalenessDays != null && clientVersionStalenessDays >= 30 && info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { | |
// Trigger FLEXIBLE flow | |
startUpdate(info, AppUpdateType.FLEXIBLE) | |
} | |
} else if (info.updatePriority() == 1) { // Priority: 1 | |
// Trigger FLEXIBLE flow | |
startUpdate(info, AppUpdateType.FLEXIBLE) | |
} else { // Priority: 0 | |
// Do not show in-app update | |
} | |
} else { | |
// UPDATE IS NOT AVAILABLE | |
} | |
} | |
appUpdateManager.registerListener(this) | |
} | |
private fun startUpdate(info: AppUpdateInfo, type: Int) { | |
appUpdateManager.startUpdateFlowForResult(info, type, parentActivity, MY_REQUEST_CODE) | |
currentType = type | |
} | |
fun onResume() { | |
appUpdateManager.appUpdateInfo.addOnSuccessListener { info -> | |
if (currentType == AppUpdateType.FLEXIBLE) { | |
// If the update is downloaded but not installed, notify the user to complete the update. | |
if (info.installStatus() == InstallStatus.DOWNLOADED) | |
flexibleUpdateDownloadCompleted() | |
} else if (currentType == AppUpdateType.IMMEDIATE) { | |
// for AppUpdateType.IMMEDIATE only, already executing updater | |
if (info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) { | |
startUpdate(info, AppUpdateType.IMMEDIATE) | |
} | |
} | |
} | |
} | |
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | |
if (requestCode == MY_REQUEST_CODE) { | |
if (resultCode != AppCompatActivity.RESULT_OK) { | |
// If the update is cancelled or fails, you can request to start the update again. | |
Log.e("ERROR", "Update flow failed! Result code: $resultCode") | |
} | |
} | |
} | |
private fun flexibleUpdateDownloadCompleted() { | |
Snackbar.make( | |
parentActivity.findViewById(R.id.activity_main_layout), | |
"An update has just been downloaded.", | |
Snackbar.LENGTH_INDEFINITE | |
).apply { | |
setAction("RESTART") { appUpdateManager.completeUpdate() } | |
setActionTextColor(Color.WHITE) | |
show() | |
} | |
} | |
fun onDestroy() { | |
appUpdateManager.unregisterListener(this) | |
} | |
override fun onStateUpdate(state: InstallState) { | |
if (state.installStatus() == InstallStatus.DOWNLOADED) { | |
flexibleUpdateDownloadCompleted() | |
} | |
} | |
} |
Job Offers
Initialize the InAppUpdate.kt class and add the methods onResume and onActivityResults to your activity (often the MainActivity):
class MainActivity : AppCompatActivity() { | |
private lateinit var inAppUpdate: InAppUpdate | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContentView(R.layout.activity_main) | |
inAppUpdate = InAppUpdate(this) | |
} | |
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { | |
super.onActivityResult(requestCode, resultCode, data) | |
inAppUpdate.onActivityResult(requestCode,resultCode, data) | |
} | |
override fun onResume() { | |
super.onResume() | |
inAppUpdate.onResume() | |
} | |
override fun onDestroy() { | |
super.onDestroy() | |
inAppUpdate.onDestroy() | |
} | |
} |
When you use this code and set priority to:
5: Instantly displays Immediate (Recommended for critical updates)
4: Displays Immediacy after 5 days and Flexibility after 3 days.
3: Displays Immediate after 30 days and Flexible after 15 days (Recommended for performance updates)
2: Displays Immediacy after 90 days and Flexibility after 30 days (Recommended for minor updates)
1: Always Shows Flexibility
0: It has no effect on the update flow.
Of course, you are free to modify the code to suit your requirements.
There is no way to set the update’s priority through the Google Play Console; instead, you must use the Google Play Developer API.
Testing
You must upload your app twice to the internal (or alpha or beta) track in order to test your in-app update solution.
Thank you for taking the time to read this article. If you found this post to be useful and interesting, please clap and recommend it.
If I got something wrong, mention it in the comments. I would love to improve.
This article was originally published on proandroiddev.com on July 08, 2022