Home · Android & Kotlin Tutorials

In-App Updates: Getting Started

Learn how to make an in-app mechanism to notify users of a new version of your app.

5/5 4 Ratings

Version

  • Kotlin 1.3, Android 5.0, Android Studio 3.5

When you create an app, you want to reach the maximum number of users. You want everyone to have the latest version installed, and you want them to use the latest features you’ve developed.

So…how long is this gonna take?

Potentially a while. Many choke points exist between users and the latest version of your app. For example, automatic updates may only validate if the device has a Wi-Fi connection and is not 3G, or the user may have disabled the option.

Both cases require them to go directly to the Play Store and search for updates. Let’s be honest: How many users do you think will take the time?

Thankfully, there’s a better way. You can overcome potential choke points by implementing an in-app update mechanism. This mechanism will notify users when a new version becomes available, so they can download it without any hassle.

Note: This tutorial assumes familiarity with Kotlin. If you’re new to Kotlin development, first check out our tutorial Kotlin For Android: An Introduction.

Getting Started

To download the project materials, click the Download Materials button at the top or bottom of this tutorial. Open the start project in Android Studio 3.5 or later.

In this tutorial, you’ll build an app to display the weather information for your town. More importantly, the app will feature an option to help users maintain the latest version.

screen_store_flexible

Examining the Project’s Structure

Open the project with Android Studio. After it synchronizes, you’ll see a screen like the one below:

screen-android-studio-begin

Compile and run. The app works but will also be mum on key information such as temperature and wind speed.

screen_no_key

Next, survey the different folders and files of the project. Browsing through the explorer on the left, you’ll see a set of subfolders and classes. Starting from the top, they are:

  • api: You’ll use OpenWeatherMap to retrieve the current weather information for your city. These classes provide an abstraction of the entire process.
  • model: You’ll use these classes to parse and hold the information received on the response body.
  • utils: You’ll use a set of functions to get general information — if the location service is enabled or not, formatting measurements, etc.
  • MainActivity.kt: You’ll use this to check if conditions are met to make a weather request. It also updates all the views and is where you’ll write the in-app update logic.

Configuring OpenWeatherMap

You’ll need an API key to request the location to OpenWeatherMap. Don’t worry. It’s free and simple to use.

Follow these steps to create an account:

  1. Go to the OpenWeatherMap API page.
  2. Click the Sign up link in the description.
  3. Create an account.

Then go to API Keys. On this page, you’ll be provided a key. Its value is unique and stored on the account you just created.

screen-openweathermap

Copy the key, and then head back to Android Studio. Open the class OpenWeatherAPI located in the api folder. You’ll see an empty constant called API_KEY. Paste the key there.

private const val API_KEY = "" //Your API key from OpenWeatherMap

The documentation on the openweathermap API can be found at the OpenWeatherMap API section.

Retrieving Weather Data

Enable the location services to retrieve the weather data. Go to the Settings app, and click on Location. Toggle the button to turn it on. It may take a while before your location data is available to the app. So ensure that the app has access to the device location first; otherwise, you will see one of the following messages:

  • Location permissions not granted.
  • Location not available, please enable it on settings.

Build and run. If the previous conditions have been met and your device is connected to the internet, you’ll see a screen similar to this one:

screen_begin

What’s the weather like in your city? :]

Implementing In-App Updates

In-app updates are a complimentary feature of Google Play’s auto-update option. They automatically download and install the latest version of your app.

But since the user defines this behavior, scenarios may arise where in-app updates may not be enough. As such, you need to provide support at the app level to inform users when a new update becomes available.

To implement this feature, your app needs to meet the following requirements:

  • The device needs to run at least Lollipop (API 21+).
  • You need to use Play Core Library 1.5.0 or newer.

At the time of writing, the current version is 1.6.4, so you’ll add this library to the dependencies section of the app’s build.gradle:

implementation 'com.google.android.play:core:1.6.4'

Click Sync Now to download the Google Play Core library.

Time to start using it.

Checking for Updates

Before diving into the different types of updates, you need to find out if one is available.

Add the following to MainActivity after onCreate.

private fun checkForUpdates() {
  //1
  val appUpdateManager = AppUpdateManagerFactory.create(baseContext)
  val appUpdateInfo = appUpdateManager.appUpdateInfo
  appUpdateInfo.addOnSuccessListener {
    //2
    handleUpdate(appUpdateManager, appUpdateInfo)
  }
}

Here’s what’s happening in that code:

  1. To know that a new update is available, you need to create an instance of AppUpdateManager and then register a listener that will be triggered when the app communicates with the Play Store.
  2. Once executed, you’re going to call handleUpdate which will check if the update can be done or not. You will add that shortly.
Note: Press option + enter on a Mac or alt + enter on a PC to import the classes after pasting the code in your IDE if they are not imported automatically. Where asked to choose from more than one package, choose the one from the com.google.android.play.core.* package.

Add the checkForUpdates() to onCreate function. It should be the last instruction in this function. If you call it in onResume, your app will ask the user to update every time it goes back to the foreground. Not exactly the user-friendly experience you’re looking for.

In-app updates can be divided into two types: immediate and flexible. Each one has its distinct update flow, so select the one that corresponds to your app’s requirements.

Currently, the Google Play Console offers no support to define the type of update for a new release. As such, this step has to be done on the app side.

In this tutorial, you’ll practice implementing both types of in-app updates. You can easily choose between one or the other. Start by adding the following before the class declaration in MainActivity:

private const val APP_UPDATE_TYPE_SUPPORTED = AppUpdateType.IMMEDIATE

Then after onCreate, add the following:

private fun handleUpdate(manager: AppUpdateManager, info: Task<AppUpdateInfo>) {
  if (APP_UPDATE_TYPE_SUPPORTED == AppUpdateType.IMMEDIATE) {
    handleImmediateUpdate(manager, info)
  } else if (APP_UPDATE_TYPE_SUPPORTED == AppUpdateType.FLEXIBLE) {
    handleFlexibleUpdate(manager, info)
  }
}

This will verify the type of update that should be done based on that previously defined constant.

Adding Immediate Updates

When an immediate update is found, Play Core displays an activity on top of your app that blocks all user interactions until the update is accepted or canceled. As such, this type of update is for critical scenarios.

When the user accepts, the system will be responsible for the entire flow from download to installation to the restart. As this flowchart shows:

InAppUpdate-Immediate

From the developer’s vantage, it will only be necessary to start the updating process and respond to the case if the user cancels the update or an error occurs.

After handleUpdate, add the following:

private fun handleImmediateUpdate(manager: AppUpdateManager, info: Task<AppUpdateInfo>) {
  //1
  if ((info.result.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE ||
    //2
    info.result.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) &&
    //3    
    info.result.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
    //4    
    manager.startUpdateFlowForResult(info.result, AppUpdateType.IMMEDIATE, this, REQUEST_UPDATE)
   } 
 }

Here’s a step-by-step rundown of the function:

  1. Before starting the update, it’s important to analyze the response from the Play Store. The update availability can be one of the following values:
    • DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS: When there’s an ongoing update.
    • UPDATE_AVAILABLE: When a new update is available.
    • UPDATE_NOT_AVAILABLE: When there’s no update available.
    • UNKNOWN: When there was a problem connecting to the app store.
  2. Before starting this process, verify that there’s an update available or one already in progress.
  3. Verify if the immediate type is supported.
  4. Start or resume the update with the startUpdateFlowForResult but only if the previous conditions are true.

You’ll need to set a request code after calling startUpdateFlowForResult which will launch an external activity. That way, when it finishes, you can check if the operation proved successful or not.

Add the constant below to MainActivity before the class declaration:

private const val REQUEST_UPDATE = 100

After calling startUpdateFlowForResult with AppUpdateType.IMMEDIATE, the Play Core activity will take care of the update and restart the app when it finishes.

You’ll see a progress bar with the current state of the update and how long it will take to finish. Hang on for a second, you’ll be testing the app soon. Before that, follow carefully to implement the flow for flexible updates.

Adding Flexible Updates

A flexible update is not as intrusive as an immediate one. You decide how to notify users about the new version, and you have more control over its flow. For Today’s Weather app, you’ll use a button. As the flowchart below demonstrates:

InAppUpdate-Flexible

Before defining the handleFlexibleUpdate, add these two views inside RelativeLayout on activity_main.xml:

<TextView
   android:id="@+id/tv_status"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_margin="16dp"
   android:layout_centerHorizontal="true"
   android:visibility="gone"
   android:fontFamily="sans-serif-condensed-medium"
   android:text="@string/info_processing"
   android:textSize="20sp" />

<Button
   android:id="@+id/btn_update"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_below="@id/tv_status"
   android:layout_centerHorizontal="true"
   android:visibility="gone"
   android:text="@string/action_update"
   android:textSize="20sp" />

TextView displays the current state of the update, and the Button triggers this action. As you can see, both views’ visibility is set as gone. They should only become visible if there’s a flexible update available.

Add handleFlexibleUpdate after handleImmediateUpdate which is responsible for implementing the flexible update type.

private fun handleFlexibleUpdate(manager: AppUpdateManager, info: Task<AppUpdateInfo>) {
  if ((info.result.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE ||
    info.result.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) &&
    info.result.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
      btn_update.visibility = View.VISIBLE
      setUpdateAction(manager, info)
    }
}

Similar to immediate updates, flexible updates first check for available updates. If one is found, the button to start the process becomes visible, and the app calls setUpdateAction to handle the flow.

However, with flexible updates, there’s no automatic update flow. The app needs to implement it.

To achieve this, first add the following before onCreate:

private lateinit var updateListener: InstallStateUpdatedListener

Then add the function below to MainActivity after handleFlexibleUpdate declaration:

private fun setUpdateAction(manager: AppUpdateManager, info: Task<AppUpdateInfo>) {
  //1
  btn_update.setOnClickListener {
    //2
    updateListener = InstallStateUpdatedListener {
      //3
      btn_update.visibility = View.GONE
      tv_status.visibility = View.VISIBLE
      //4
      when (it.installStatus()) {
        InstallStatus.FAILED, InstallStatus.UNKNOWN -> {
          tv_status.text = getString(R.string.info_failed)
          btn_update.visibility = View.VISIBLE
        }
        InstallStatus.PENDING -> {
          tv_status.text = getString(R.string.info_pending)
        }
        InstallStatus.CANCELED -> {
          tv_status.text = getString(R.string.info_canceled)
        }
        InstallStatus.DOWNLOADING -> {
          tv_status.text = getString(R.string.info_downloading)
        }
        //5
        InstallStatus.DOWNLOADED -> {
          tv_status.text = getString(R.string.info_installing)
          launchRestartDialog(manager)
        }
        InstallStatus.INSTALLING -> {
          tv_status.text = getString(R.string.info_installing)
        }
        //6
        InstallStatus.INSTALLED -> {
          tv_status.text = getString(R.string.info_installed)
          manager.unregisterListener(updateListener)
        }
        else -> {
          tv_status.text = getString(R.string.info_restart)
        }
      }
    }
    //7
    manager.registerListener(updateListener)
    //8
    manager.startUpdateFlowForResult(info.result, AppUpdateType.FLEXIBLE, this, REQUEST_UPDATE)
  }
}

Here’s a step-by-step code analysis:

  1. First, it sets the callback for when the user taps the button. This action will start the update action defined in step 7.
  2. It defines InstallStateUpdateListener, which notifies the app of every step of the process.
  3. tv_status displays visual information about the update. It’s hidden by default.
  4. The when block defines all the possible states on the update flow.
  5. When the system finishes downloading the .apk, the app can either be in one of two states:
    • If on foreground, the user needs to confirm that the app can be relaunched. This avoids interrupting their current usage of the app.
    • If on background, the user minimizes it after declining the installation. The system will automatically install the newest update and relaunch when the app returns to foreground.
  6. After the update is successfully installed, there’s no need to keep the listener register in the app. There are no more actions to be made, so you can hide the Button and unregister the callback.
  7. Register the previous listener.
  8. Start the flexible update.

When the installation is completed, implement a dialog to inform the user that the newest version of the app is ready to be installed. For that, it needs to be relaunched. Add the following after the setUpdateAction function.

private fun launchRestartDialog(manager: AppUpdateManager) {
  AlertDialog.Builder(this)
    .setTitle(getString(R.string.update_title))
    .setMessage(getString(R.string.update_message))
    .setPositiveButton(getString(R.string.action_restart)) { _, _ ->
      manager.completeUpdate()
    }
    .create().show()
}

Add the function below after setUpdateAction:

AppUpdateManager.completeUpdate()

When the user clicks on the dialog button, the app will restart.

Handling User Actions

Since it’s possible to cancel an update, it’s important to override onActivityResult and check the resultCode received so the app can respond accordingly.

This scenario is valid for both types of updates. Add the following block of code after the launchRestartDialog function:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  //1
  if (REQUEST_UPDATE == requestCode) {
    when (resultCode) {
      //2
      Activity.RESULT_OK -> {
        if (APP_UPDATE_TYPE_SUPPORTED == AppUpdateType.IMMEDIATE) {
          Toast.makeText(baseContext, R.string.toast_updated, Toast.LENGTH_SHORT).show()
        } else {
          Toast.makeText(baseContext, R.string.toast_started, Toast.LENGTH_SHORT).show()
        }
      }
      //3
      Activity.RESULT_CANCELED -> {
        Toast.makeText(baseContext, R.string.toast_cancelled, Toast.LENGTH_SHORT).show()
      }
      //4
      ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> {
        Toast.makeText(baseContext, R.string.toast_failed, Toast.LENGTH_SHORT).show()
      }
   }
  super.onActivityResult(requestCode, resultCode, data)
 }
}

In this scenario, you’ll just show a toast depending on the user action:

  1. Confirm that onActivityResult was called with requestCode from an in-app update. It should be the same code as the one defined in startUpdateFlowForResultREQUEST_UPDATE.
  2. The update was successfully installed.
  3. The user canceled the update. Although it’s showing a toast, as a challenge, you can show a dialog mentioning the importance of always installing the latest version.
  4. The update failed due to some unknown reason. Typically, this is an error from the Play Store. Try to restart the update process.

Testing

You’ve written a lot of code, but how do you test that everything works as expected? Ideally, you publish the app on the Play Store, and then you increment its version code and launch another one.

But this process can be time-consuming, so you should test the app locally to guarantee everything works as expected.

To mock this flow, use FakeUpdateManager but configured for both types of updates. You’ll use BuildConfig.DEBUG to define when to run this mocked scenario, but remember that it should only be executed in debug builds.

Change how AppUpdateManager is initialized in checkForUpdates by replacing it with the code below:

val appUpdateManager : AppUpdateManager
if (BuildConfig.DEBUG) {
  appUpdateManager = FakeAppUpdateManager(baseContext)
  appUpdateManager.setUpdateAvailable(2)
} else {
  appUpdateManager = AppUpdateManagerFactory.create(baseContext)
}

With this, you’ll use the Play Store’s AppUpdateManager when the app is launched and FakeAppUpdateManager when tested locally.

The number 2 on setUpdateAvailable corresponds to the app version code. It can be any number as long as it’s higher than your version code defined on build.gradle.

Now, to simulate an immediate update, add this logic to the end of handleImmediateUpdate.

if (BuildConfig.DEBUG) {
  val fakeAppUpdate = manager as FakeAppUpdateManager
  if (fakeAppUpdate.isImmediateFlowVisible) {
    fakeAppUpdate.userAcceptsUpdate()
    fakeAppUpdate.downloadStarts()
    fakeAppUpdate.downloadCompletes()
    launchRestartDialog(manager)
  }
}

It’s necessary to make the above calls to mimic the entire flow. Only after calling startUpdateFlowForResult will isImmediateFlowVisible return true and the rest of the update can be mocked.

Compile and run. You’ll see a similar screen:

screen_fake_immediate

The process is identical to the flexible type. Add these instructions right after the startUpdateFlowForResult in setUpdateAction:

if (BuildConfig.DEBUG) {
  val fakeAppUpdate = manager as FakeAppUpdateManager
  if (fakeAppUpdate.isConfirmationDialogVisible) {
    fakeAppUpdate.userAcceptsUpdate()
    fakeAppUpdate.downloadStarts()
    fakeAppUpdate.downloadCompletes()
    fakeAppUpdate.completeUpdate()
    fakeAppUpdate.installCompletes()
  }
}

Change APP_UPDATE_TYPE_SUPPORTED to AppUpdateType.FLEXIBLE.

Build and run. You should see a workable update button.

screen_fake_flexible

Note: Don’t change your build variant from debug to release.

Launching an Update

FakeUpdateManager allows you to make the first offline tests, but you can also publish your app on the store to see it working in a real scenario.

You can learn more about publishing an Android app with the Android App Distribution Tutorial: From Zero to Google Play Store tutorial.

Don’t forget that the app published should have a higher version code and name than the one you use to test from your computer.

For this, open build.gradle and update these values:

versionCode 2
versionName "1.1"

You can now publish your new version of the app on the Google Play Store.

When testing from the Play Store, you’ll see this screen for immediate updates:

screen_store_immediate

And you’ll see this one for flexible updates:

screen_store_flexible

Troubleshooting

Because updates can be distributed across multiple servers, the process of receiving one may take a while. With continuous testing, the latest version may become cached on your Play Store. It’s necessary to then clear it, so you can receive a notification when a new version is available.

You can do this via one of two options:

  • Google Play Store app ▸ My apps & games
  • Native Settings ▸ Apps ▸ Google Play Store ▸ Storage & cache ▸ Clear cache

Additionally, if you use app Google Play’s app signing, there’s a known problem where the update will fail on the installation phase. This happens because Google creates a derived .apk that is resigned. This security measure makes the cross-validation fail.

In such a scenario, both installers are signed with the same key, but they don’t match. This results in the update being aborted.

Note: It’s only possible to detect this scenario by analyzing the messages on Logcat. Look for INSTALL_FAILED_UPDATE_INCOMPATIBLE.

After this, the next time you open your app, you’ll see the update screen again.

Where to Go From Here?

You’ve just implemented a mechanism to notify your users of updates. With it, they can always enjoy the latest, greatest version of your app. Congrats!

So, what does the future hold for in-app updates?

During the 2019 Android Dev Summit, Google announced three new features for the Google Play Console:

  • Priority: This feature will make it possible to define the type of update you’ll support on the Play Console. You’ll have both implementations written on apps.
  • Staleness: This feature will return an integer with the number of days an incoming update has been ignored.
  • Progress: If you’re doing a flexible update, you currently have no API to give you the number of bytes already downloaded along with the update size.

Each of these features aims to improve in-app updates and is expected to launch soon.

If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

4 ratings

More like this

Contributors

Comments