Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Hotfix Heaven: Why In-App Updates Are a MUST

Hotfix Heaven: Why In-App Updates Are a MUST

enhanced security – all directly within your app.

This talk equips you to master In-App Updates for a thriving app:

- Become an update champion: Deliver instant fixes and keep users protected and satisfied.
- Navigate API migrations with ease: Ensure flawless user experience with seamless versioning and rollouts.
- Unlock developer efficiency: Streamline workflows, reduce friction, and empower users with immediate updates.
- UX in focus: We'll explore the impact of In-App Updates on user experience, including best practices for user buy-in.
- Strategic updates: Discover optimal update frequency to balance user engagement and retention while minimizing drop-off.

Gain real-world insights:

- Battle-tested strategies to supercharge both developer and user experience with In-App Updates.
- Hands-on implementation: Learn how to effortlessly set up In-App Updates on Android.

Who should attend?

- Android developers seeking faster, more efficient updates.
- Anyone passionate about boosting app security, stability, and user experience.
- Developers eager to learn practical implementation tactics for In-App Updates.

Join me and unlock the power of In-App Updates! Take your app development to the next level and become an update champion.

James Cullimore

September 30, 2024
Tweet

More Decks by James Cullimore

Other Decks in Programming

Transcript

  1. • What are in-app updates? ◦ Flexible vs Immediate •

    Why are in-app updates a MUST? ◦ Personal experiences • Implementation ◦ In-app updates ◦ Triggers ▪ Google Play Publishing API ▪ Firebase Remote Config ◦ Testing ◦ Troubleshooting • Statistics • Strategies
  2. What Are In-App Updates? • Google Play Core libraries feature

    • Delivers updates more promptly • Reminders for users to update • User stays within app • Android 5.0 (API level 21) or higher • Android mobile devices, Android tablets, and ChromeOS devices only • No support for .oob
  3. Flexible vs. Immediate Updates • Flexible updates ◦ Allows users

    to continue ◦ Downloads in the background ◦ Ideal for non critical updates • Immediate updates ◦ Normally used to block users from continuing ◦ Ideal for fixes and security issues
  4. Why Are In-App Updates a MUST? • Ensures timely delivery

    ◦ Automatic updates • Increases engagement & retention • Enhances UX ◦ User stays within app • Strategic control ◦ API migrations ◦ Avoid penalties in regulated environments
  5. Personal Experiences • My whoopsie • Team whoopsie • Third

    party whoopsie “Beware of bugs in the above code; I have only proved it correct, not tried it.” - Donald Knuth
  6. Implementing In-App Updates // In your app’s build.gradle.kts file: dependencies

    { implementation("com.google.android.play:app-update:2.1.0") implementation("com.google.android.play:app-update-ktx:2.1.0") }
  7. Implementing In-App Updates val appUpdateManager = AppUpdateManagerFactory.create(context) // Returns an

    intent object that you use to check for an update. val appUpdateInfoTask = appUpdateManager.appUpdateInfo // Checks that the platform will allow the specified type of update. appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE // This example applies an immediate update. To apply a flexible update // instead, pass in AppUpdateType.FLEXIBLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) ) { // Request the update. } }
  8. Implementing In-App Updates val appUpdateManager = AppUpdateManagerFactory.create(context) // Returns an

    intent object that you use to check for an update. val appUpdateInfoTask = appUpdateManager.appUpdateInfo // Checks whether the platform allows the specified type of update, // and current version staleness. appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && (appUpdateInfo.clientVersionStalenessDays() ?: -1) >= DAYS_FOR_FLEXIBLE_UPDATE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) { // Request the update. } }
  9. Implementing In-App Updates val appUpdateManager = AppUpdateManagerFactory.create(context) // Returns an

    intent object that you use to check for an update. val appUpdateInfoTask = appUpdateManager.appUpdateInfo // Checks whether the platform allows the specified type of update, // and checks the update priority. appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.updatePriority() >= 4 /* high priority */ && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) { // Request an immediate update. } }
  10. Implementing In-App Updates appUpdateManager.startUpdateFlowForResult( // Pass the intent that is

    returned by 'getAppUpdateInfo()'. appUpdateInfo, // an activity result launcher registered via registerForActivityResult activityResultLauncher, // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for // flexible updates. AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build() )
  11. Implementing In-App Updates appUpdateManager.startUpdateFlowForResult( // Pass the intent that is

    returned by 'getAppUpdateInfo()'. appUpdateInfo, // an activity result launcher registered via registerForActivityResult activityResultLauncher, // Or pass 'AppUpdateType.FLEXIBLE' to newBuilder() for // flexible updates. AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE) .setAllowAssetPackDeletion(true) .build() )
  12. Implementing In-App Updates registerForActivityResult(StartIntentSenderForResult()) { result: ActivityResult -> // handle

    callback -> RESULT_OK, RESULT_CANCELED, // ActivityResult.RESULT_IN_APP_UPDATE_FAILED if (result.resultCode != RESULT_OK) { log("Update flow failed! Result code: " + result.resultCode); // If the update is canceled or fails, // you can request to start the update again. } }
  13. Implementing In-App Updates // Create a listener to track request

    state updates. val listener = InstallStateUpdatedListener { state -> // (Optional) Provide a download progress bar. if (state.installStatus() == InstallStatus.DOWNLOADING) { val bytesDownloaded = state.bytesDownloaded() val totalBytesToDownload = state.totalBytesToDownload() // Show update progress bar. } // Log state or install the update. } // Before starting an update, register a listener for updates. appUpdateManager.registerListener(listener) // Start an update. // When status updates are no longer needed, unregister the listener. appUpdateManager.unregisterListener(listener)
  14. Implementing In-App Updates // Checks that the update is downloaded

    in 'onResume()'. // However, you should execute this check at all app entry points. override fun onResume() { super.onResume() appUpdateManager .appUpdateInfo .addOnSuccessListener { appUpdateInfo -> // If the update is downloaded but not installed, // notify the user to complete the update. if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) { popupSnackbarForCompleteUpdate() } } }
  15. Implementing In-App Updates // Checks that the update is not

    stalled during 'onResume()'. // However, you should execute this check at all entry points into the app. override fun onResume() { super.onResume() appUpdateManager .appUpdateInfo .addOnSuccessListener { appUpdateInfo -> if (appUpdateInfo.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS ) { // If an in-app update is already running, resume the update. appUpdateManager.startUpdateFlowForResult( appUpdateInfo, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()) } } }
  16. Google Developer API • Allows you to perform a number

    of publishing and app-management tasks ◦ Subscriptions and In-App Purchases API ◦ Publishing API ▪ Uploading new versions of an app ▪ Releasing apps, by assigning APKs to various Tracks (alpha, beta, staged rollout, or production) ▪ Creating and modifying Google Play Store listings, including localized text and graphics and multi-device screenshots
  17. Google Developer Publishing API • Edit.tracks: update { "releases": [{

    "versionCodes": ["88"], "inAppUpdatePriority": 5, "status": "completed" }] }
  18. Firebase Remote Config • Dynamic Configuration ◦ Key-Value Data Store

    ◦ Remote Updates ◦ Targeted Rollouts ◦ Offline Caching
  19. Firebase Remote Config dependencies { // Import the BoM for

    the Firebase platform implementation(platform("com.google.firebase:firebase-bom:33.1.0")) // Add the dependencies for the Remote Config and Analytics libraries // When using the BoM, you don't specify versions in Firebase library dependencies implementation("com.google.firebase:firebase-config") implementation("com.google.firebase:firebase-analytics") }
  20. Firebase Remote Config val remoteConfig: FirebaseRemoteConfig = Firebase.remoteConfig val configSettings

    = remoteConfigSettings { minimumFetchIntervalInSeconds = 3600 } remoteConfig.setConfigSettingsAsync(configSettings) remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
  21. Firebase Remote Config remoteConfig.fetchAndActivate() .addOnCompleteListener(this) { task -> if (task.isSuccessful)

    { val updated = task.result Log.d(TAG, "Config params updated: $updated") showUpdateFlow() } else { Toast.makeText( This, "Fetch failed", Toast.LENGTH_SHORT, ).show() } }
  22. Firebase Remote Config remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener { override fun onUpdate(configUpdate

    : ConfigUpdate) { Log.d(TAG, "Updated keys: " + configUpdate.updatedKeys); if (configUpdate.updatedKeys.contains("update_version")) { remoteConfig.activate().addOnCompleteListener { showUpdateFlow() } } } override fun onError(error : FirebaseRemoteConfigException) { Log.w(TAG, "Config update error with code: " + error.code, error) } })
  23. Firebase Remote Config val appUpdateManager = AppUpdateManagerFactory.create(context) // Returns an

    intent object that you use to check for an update. val appUpdateInfoTask = appUpdateManager.appUpdateInfo // Checks whether the platform allows the specified type of update, // and checks the update priority. appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) && (appUpdateInfo.updatePriority() in > 4 || (FirebaseRemoteConfig.getInstance().getDouble(MIN_IMMEDIATE_VERSION) > BuildConfig.VERSION_CODE)) ) { // Request an immediate update. } }
  24. Manually Testing In-App Updates • Activate internal app sharing ◦

    Play store -> menu -> settings -> tap about 7 times -> activate the setting -> voilà! • Upload app (version n) to internal app sharing • Install app (version n) via internal app sharing url • Upload app (version n+1) to internal app sharing • Make any required remote config or API changes for in-app updates • Click the sharing link for the updated version. DO NOT INSTALL! • Open the current version of the app (version n) • Make a change, rinse and repeat
  25. FakeAppUpdateManager • A fake implementation of the AppUpdateManager • Completely

    self-contained • No UI is shown • No update is performed on the device • Test initiating update flows, checking download progress, and handling failure scenarios • Not intended for full stack integration tests
  26. FakeAppUpdateManager @Before fun setUp() { // Replace the real AppUpdateManager

    with FakeAppUpdateManager val context = InstrumentationRegistry.getTargetContext() val fakeAppUpdateManager = FakeAppUpdateManager(context) AppUpdateManagerFactory.inject(fakeAppUpdateManager) }
  27. FakeAppUpdateManager @Test fun testFlexibleUpdate_Completes() { fakeAppUpdateManager.partiallyAllowedUpdateType = AppUpdateType.FLEXIBLE fakeAppUpdateManager.setUpdateAvailable(2) ActivityScenario.launch(MainActivity::class.java)

    // Validate that flexible update is prompted to the user. assertTrue(fakeAppUpdateManager.isConfirmationDialogVisible) // Simulate user's and download behavior. fakeAppUpdateManager.userAcceptsUpdate() fakeAppUpdateManager.downloadStarts() fakeAppUpdateManager.downloadCompletes() … }
  28. FakeAppUpdateManager @Test fun testFlexibleUpdate_Completes() { … // Perform a click

    on the Snackbar to complete the update process. onView( allOf( isDescendantOfA(instanceOf(Snackbar.SnackbarLayout::class.java)), instanceOf(AppCompatButton::class.java) ) ).perform(ViewActions.click()) // Validate that update is completed and app is restarted. assertTrue(fakeAppUpdateManager.isInstallSplashScreenVisible) fakeAppUpdateManager.installCompletes() }
  29. FakeAppUpdateManager @Test fun testImmediateUpdate_Completes() { fakeAppUpdateManager.partiallyAllowedUpdateType = AppUpdateType.IMMEDIATE fakeAppUpdateManager.setUpdateAvailable(2) ActivityScenario.launch(MainActivity::class.java)

    // Validate that immediate update is prompted to the user. assertTrue(fakeAppUpdateManager.isImmediateFlowVisible) // Simulate user's and download behavior. fakeAppUpdateManager.userAcceptsUpdate() fakeAppUpdateManager.downloadStarts() fakeAppUpdateManager.downloadCompletes() // Validate that update is completed and app is restarted. assertTrue(fakeAppUpdateManager.isInstallSplashScreenVisible) }
  30. FakeAppUpdateManager @Test fun testFlexibleUpdate_DownloadFails() { fakeAppUpdateManager.partiallyAllowedUpdateType = AppUpdateType.FLEXIBLE fakeAppUpdateManager.setUpdateAvailable(2) ActivityScenario.launch(MainActivity::class.java)

    // Validate that flexible update is prompted to the user. assertTrue(fakeAppUpdateManager.isConfirmationDialogVisible) // Simulate user's and download behavior. fakeAppUpdateManager.userAcceptsUpdate() fakeAppUpdateManager.downloadStarts() fakeAppUpdateManager.downloadFails() … }
  31. FakeAppUpdateManager @Test fun testFlexibleUpdate_DownloadFails() { … // Perform a click

    on the Snackbar to retry the update process. onView( allOf( isDescendantOfA(instanceOf(Snackbar.SnackbarLayout::class.java)), instanceOf(AppCompatButton::class.java) ) ).perform(ViewActions.click()) // Validate that update is not completed and app is not restarted. assertFalse(fakeAppUpdateManager.isInstallSplashScreenVisible) // Validate that Flexible update is prompted to the user again. assertTrue(fakeAppUpdateManager.isConfirmationDialogVisible) }
  32. Troubleshooting • isUpdateTypeAllowed ◦ No way of setting the type

    remotely ◦ Returns false when device space is too low or when offline • Excessive update flows ◦ Make sure to test the back off & staleness ◦ Try not to pester your users • Remote config changes not visible ◦ The throttle limit may have been reached • Update not visible in play store ◦ Empty the cache
  33. Update Frequency • Banking App ◦ Usage ▪ Weekly ▪

    10 Minutes ◦ Ideal Interval ▪ Monthly ◦ Frequency Cap ▪ Weekly ◦ Back off ▪ 3 days • Social Media App ◦ Usage ▪ Daily ▪ 1 Hour ◦ Ideal Interval ▪ Bi-Weekly / Monthly ◦ Frequency Cap ▪ Daily ◦ Back off ▪ 7 days • Idle Game ◦ Usage ▪ 2-3 Days ▪ 2 Hours ◦ Ideal Interval ▪ Weekly / Bi-weekly ◦ Frequency Cap ▪ Daily ◦ Back off ▪ 1 day
  34. Update Strategies • In-app updates ◦ Use them, NOW! ◦

    Ideally, from the start • Track metrics ◦ How many installed or cancelled which update? when? and how? • A/B Testing • Get feedback regularly ◦ Reviews or Survicate ◦ Prioritize tasks accordingly • Utilise app bundles • Aim for value, every release • Clear communication ◦ Release notes
  35. Summary • What in-app updates are • Why there are

    a MUST • How to implement it • How to trigger them • How to test (ideally) • Not everyone gets updates • Better insight into frequency • Strats, for better impact
  36. References • https://developer.android.com/guide/playcore/in-app-updates • https://developer.android.com/guide/playcore/in-app-updates/kotlin-java • https://developer.android.com/reference/com/google/android/play/core/release-notes-in_app_upd ates • https://developers.google.com/android-publisher/tracks#apk_workflow_example

    • https://firebase.google.com/docs/remote-config/get-started?platform=android • https://firebase.google.com/docs/remote-config/get-started?platform=android#throttling • https://firebase.google.com/docs/remote-config/automate-rc • https://developer.android.com/reference/com/google/android/play/core/install/model/InstallStatu s • https://developer.android.com/reference/com/google/android/play/core/appupdate/testing/FakeA ppUpdateManager.html • https://www.velvetech.com/blog/mobile-app-update/ • https://www.researchgate.net/publication/282501830_Fresh_apps_An_empirical_study_of_frequent ly-updated_mobile_apps_in_the_Google_play_store
  37. References • https://www.androidauthority.com/auto-update-apps-google-play-store-poll-results-1077403/ • https://www.dotcominfoway.com/blog/infographic-why-users-uninstall-your-app/#gref • https://www.researchgate.net/publication/364437842_RoseMatcher_Identifying_the_Impact_of_Us er_Reviews_on_App_Updates • https://www.appsflyer.com/resources/reports/app-uninstall-benchmarks/

    • https://www.statista.com/statistics/1351023/us-mobile-gamers-uninstalling-apps-reasons/ • https://habr.com/en/companies/vk/articles/452082/ • https://blog.droidchef.dev/working-with-in-app-updates-in-android/ • https://medium.com/wantedly-engineering/testing-android-in-app-updates-with-fakeappupdateman ager-63d0e834c36 • https://github.com/malvinstn/FakeAppUpdateManagerSample • https://academy.droidcon.com/course/triggering-in-app-updates-using-firebase-remote-config-in-an droid