How to implement In-App Updates in Android using Kotlin

Last updated on: April 12, 2021

There’re two ways to show an update inside your app:

Flexible: A popup appears, asking the user if they want to update the app. They can accept or deny it. If they accept it, the update will download in the background. This can be used when your update has some minor UI changes or performance improvements.

Immediate: This is a full-screen UX that requires the user to update the app to continue using it. This can be used when you have a critical update, like a security fix.

You can trigger the update with two signals:

Priority:  In each release, you define how important the update is by giving an integer that goes between 0 and 5 (5 being the highest priority). This will trigger the right update flow (Immediate or Flexible) to update the app.

Staleness: Says how long the device has known about an update being available. This helps you to trigger the right flow. For example, if the user hasn’t updated the app in the last 30 days since the release of the update, then trigger the Flexible flow, and if it’s longer than 90 days, trigger the Immediate flow.

You can also combine both signals for a better user experience, and this is what we’re going to implement in this tutorial.

Adding library

Go to your module-level gradle.build file, and add the following dependencies:

dependencies {
    // ...

    implementation 'com.google.android.material:material:1.2.1'
    implementation 'com.google.android.play:core:1.9.0'

    // ...
}Code language: Kotlin (kotlin)

We need the material library to show a snackbar when the update has been completed when we’re using the flexible flow.

Implementing in-app update

To make it easier to implement, we’re going to add all the things we need in a different file, and then we’re going to call it from the Activity we want to check for the updates.

Create a new file, name it InAppUpdate, and paste the following code inside:

package com.example.inappupdatesexample

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()
        }
    }

}Code language: Kotlin (kotlin)

In your Activity (usually is the MainActivity), initialize the InAppUpdate class, and add the methods onResume and onActivityResults:

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()
    }
}Code language: Kotlin (kotlin)

Using this code, when you set priority to:

5: Shows Immediate instantly (Recommended for critical updates)
4: Shows Flexible after 3 days and Immediate after 5 days
3: Shows Flexible after 15 days and Immediate after 30 days (Recommended for performance updates)
2: Shows Flexible after 30 days and Immediate after 90 days (Recommended for minor updates)
1: Shows Flexible forever
0: It doesn’t trigger any update flow

Of course, you can change the code the way you like to fit your needs.

To set the priority for the update, you need to do it through Google Play Developer API, and there’s no option to do it from the Google Play Console (yet).

Instead of using the Google Play Developer API, we’re going to use the Gradle Play Publisher. It’s a Gradle plugin that automates the process of publishing an app on the Google Play Store, and we can set the priority for our release from inside the Android Studio.

NOTE: The Gradle Play Publisher only works for apps that have already been published on the Google Play Store. If you haven’t published it yet. You have to do it through the Google Play Console and then use the Gradle Play Publisher for the next releases.

Before we start with this, we need to have a key(.json) file, that we’ll get after creating a Service account in Google Play Console, and the Keystore(.jks) file.

Creating a Service Account in Google Play Console

In Google Play Console, go Settings > API Access press Choose a project to link, and then I agree

Next, go Create new project and press the Link project button

Press Create new service account and open the Google Cloud Platform

In Google Cloud Platform, press + CREATE SERVICE ACCOUNT

Give it a name, and press CREATE

In the next step, select Owner as a role (don’t worry, we’ll remove it later), and press Done

Press the 3 dots on the right side, and press Create key

On the new window, select JSON and CREATE

Rename the .json file to a simpler name, like key.json, and save it in your computer (or in the cloud, like Dropbox, because we’re going to add it in the .gitignore later and every time you download your project from version control, you need to add it back in your project manually)

Now, go back to Google Play Console and press Done

Next, go to the Users and permissions section and press Invite new users

Open the .json file you downloaded before (key.json) with a text editor, copy the email from “client_email” and paste it inside the Email address.

Then, press Add app and select your app from the list

On the new window, select the followings and press Apply

With these permissions, we’ll be able to publish our apps for testing and production using the Gradle Play Publisher.

Next, press Invite user

Now, it’s time to remove the Owner permission we added previously.

Go back to Google Cloud Platform, select the IAM section from the left sidebar, and press the pencil icon to edit the account

On the new window, press the trash icon to remove the permission and then SAVE

Done! We created the service account!

Now let’s copy the key(.json) and the Keystore(.jks) file into our project.

Adding your Service Account and Keystore files in your project

Go to your project’s folder, and inside the app folder, paste the key(.json) and the Keystore(.jks) file:

Next, to edit the .gitignore, we need to unhide the hidden files, press Cmd + Shift + . (dot) and open the file with a text editor (e.g., VS Code), and add the following lines (the .jks lines might be already there, just uncomment them)

We’re doing this because when you have your app on version control, and it’s not a private repo, then everyone can have access to your Google Play Developer account.

Gradle Play Publisher

Adding the plugin

Go to your module-level gradle.build file, and at the top of your file:

If you’re using the plugins DSL, add:

plugins {
  // …

  id "com.github.triplet.play" version "3.1.0"
}Code language: Swift (swift)

If you’re using legacy plugin application, in your top-level build.gradle file add:

buildscript {
  repositories {
    maven { url https://plugins.gradle.org/m2/ }
  }
  dependencies {
    classpath "com.github.triplet.gradle:play-publisher:3.1.0"
  }
}Code language: Swift (swift)

And then, in your module-level build.gradle add:

// …
apply plugin: "com.github.triplet.play"
Code language: Swift (swift)

Setting up the plugin

In the module-level build.gradle file, inside the android {  } , and before buildTypes add:

// defaultConfig …

signingConfigs {
    release {
        storeFile file("keystore.jks")
        storePassword "MY_KEYSTORE_PASSWORD"
        keyAlias "MY_KEY_ALIAS"
        keyPassword "MY_KEY_PASSWORD"
    }
}

// buildTypes …
Code language: Swift (swift)

Inside the buildTypes, add:

buildTypes {
    release {
        signingConfig signingConfigs.release         
        // ...
    }
}Code language: Swift (swift)

And after buildTypes, add:

// buildTypes …

play {
    track.set("internal") // internal / alpha / beta / production
    updatePriority.set(5) // Between 0 and 5
    serviceAccountCredentials.set(file("key.json"))
}
Code language: Swift (swift)

Replace the storePassword, keyAlias, and keyPassword with your own keys.

Next, if you want to include Release Notes with your release, create a .txt file with the name of the track (e.g., internal.txt, beta.txt, etc.) and paste it inside:

(your project name)/app/src/main/(create a folder and name it play)/(create a folder and name it release-notes)/(create a folder and name it with the language of the release notes, for example, en-US)

Testing

To test your in-app update implementation, you need to upload your app on the internal (or alpha or beta) track, twice.

Go to your module-level build.gradle file, increase the versionCode and the versionName, set your track, and press Sync Now

Then, open the Terminal inside the Android Studio and type ./gradlew publishBundle to publish a bundle.

If you want to publish a .apk, type ./gradlew publishApk

See more details about Gradle Publish Plugin’s terminal commands here

Now, go to your Google Play Console to see the release in the track you set before (Mine is the ‘internal’ track)

Select the Testers tab to add your testers

Press Create email list and add the email address you have on your phone with Google Play Store installed.

Then press the Copy link and open it from your phone to have access to the internal track.

After installing your app through the internal track, do the same steps we did (increase versionCode, upload with the Gradle Play Publisher, etc.), but now, set a number for the priority of the update.

Go to Google Play Store on your phone and check if your app has an update (DO NOT update it from there).

Close the Google Play Store and open your app to see the in-app update.

From now on, this is the way that you have to upload your app (through Gradle Play Publisher). If you upload it from Google Play Console, the app won’t trigger any in-app update flow to the users.

You can find the final project here

If you have any questionsplease feel free to leave a comment below

Subscribe
Notify of
guest
36 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments