Understanding in-app updates in Flutter

A few months ago, we mistakenly pushed out a poorly thought out feature that would ruin the experience of the user. Once we realized this we pushed a reverted release immediately. The best thing about this was we were sure the user would be updated almost immediately because of the in-app update flow we implemented.

In here lies the importance of in-app updates. Mistakes happen, security issues and slight bugs can be pushed to production. With in-app updates, you can make sure your users are always on the cutting edge with you. So at this point, you're probably wondering what exactly is in-app update. Well then, let's get down to business.

In-app Updates

Now that we've established the importance of app updates, We'll know focus on the implementation. The Android OS provides a feature known as In-app Updates that allows your users to simultaneously update and use your app at the same time.

It ensures your users stay on the most current and safest version of your application in the most comfortable way possible. This article will help you accurately setup in-app updates for your application.

Let's get you up to speed : )

Setup

First of all, the plugin of our choice. in_app_updates. Should be added to your pubspec.yaml in the normal convention:


    dependencies:
        in_app_update: ^2.0.0

:::info The latest version is 2.0.0 at the time of writing this article. :::

Secondly, let's look at the folder structure of our application, a simple three-file project

Use

At the top of your preferred dart file, which in this case will be our service class, update_service.dart, import:


    import 'package:in_app_update/in_app_update.dart';

It's time for the actual work so let's talk about the flow. When the user opens your application:

  • You check for any new versions
  • New version found:
    • Start preferred method of updating.
    • Complete update and proceed to the app
    • Catch and deal with errors that occur during this process
  • No version found, continue entry into the app.

:::info :bulb: The two methods for updating are Immediate and Flexible updates. We'll come to that later

:::

Now that we've established our implementation path, it's about time we write some code right? Okay!

Check for new version

I prefer if this check is done in the initState() method of the screen that comes up immediately after your splash screen has been shown:

  @override
  void initState() {
    super.initState();

    _startUpdateService();
  }

  //..

  Future<void> _startUpdateService() async {
    try {
      //This feature is only available on the Android OS
      //As specified above.

      if (Platform.isAndroid) {
        //This runs the code to start the immediate update service
        await UpdateService.startImmediateUpdate();
      }
    } catch (e) {
      //Your preferred method of showing the user erros
    }
  }

In a bit to ensure a degree of decopuling, We'll write the core update logic in a class called update_service.dart, aptly named UpdateService

Once again, make sure to import:


    import 'package:in_app_update/in_app_update.dart';

Checking for app update logic:


  //For checking updates
  static Future<AppUpdateInfo?> _checkForUpdate() async {
    try {
      return await InAppUpdate.checkForUpdate();
    } catch (e) {
      //Throwing the exception so we can catch it on our UI layer
      throw e.toString();
    }
  }

Here, we're checking for updates via InAppUpdate.checkForUpdate() and catching any possible errors via the try/catch loop.

Update if new version exists: Immediate Update

As mentioned earlier, we'll be studying immediate updates first.

Immediate updates ensures that your user download the update before they even get the chance to use your application. The logic is written below:


static Future<void> checkForImmediateUpdate() async {
    try {
        //Call and get the result from the initial function
        final AppUpdateInfo? info = await _checkForUpdate();

        //Because info could be null
        if (info != null) {
            if (info.updateAvailability == UpdateAvailability.updateAvailable) {
                InAppUpdate.performImmediateUpdate();
            } 
        }
    } catch (e) {
        throw e.toString();
    }
}

Here we're starting an immediate update if there's an update available. This shows a full screen dialog to the user, specifying the size and network options with which to download the update.

Screenshot:

Update if new version exists: Flexible Update

In this update flow, we'll first show the user a dialog specifying whether they want to update their app or not. This initial dialog can be prompted with the code below:


static Future<void> checkForFlexibleUpdate() async {
    //Here, you'll show the user a dialog asking if the user is wiilling
    //to update your app while using it

    try {
      //We'll check if the there is an actual update
      final AppUpdateInfo? info = await _checkForUpdate();

      //Because info could be null. If info is null, we can safely assume that there is no pending
      //update

      if (info != null) {
        if (info.updateAvailability == UpdateAvailability.updateAvailable) {
          //The user starts the flexible updates, when it's done we'll ask the user if they want to go ahead with the update or not
          await InAppUpdate.startFlexibleUpdate();
        }
      }
    } catch (e) {
      throw e.toString();
    }
  }

After the update is downloaded in the background, the user can then choose to complete the update process. Don't worry about the call flow of these methods, we'll come to that later.

For now, let's review the logic to complete the update proces.


static Future<void> completeFlexibleUpdate() async {
    //Here, the user has downloaded and queued up your new update, it's time to
    //actually install it!

    try {
      await InAppUpdate.completeFlexibleUpdate();
    } catch (e) {
      throw e.toString();
    }
  }

Now let's put this usecase in complete code form. Preferrably in the initState() method of your home_screen.dart screen:


  @override
  void initState() {
    super.initState();

    _startUpdateService();
  }

  Future<void> _startUpdateService() async {
    try {
      //This feature is only available on the Android OS
      //As specified above.

      if (Platform.isAndroid) {
         //Here, the user is asked and could decide if they want to start the update or ignore it
        await UpdateService.checkForFlexibleUpdate();

        //After this is done, we'll show a dialog telling the user
        //that the update is done, and ask them to complete it.
        //For this case, we'll use a simple dialog to demonstrate this
        showDialog(
          context: context,
          builder: (context) => AlertDialog(
            content: Text("Your update has been downloaded"),
            actions: [
              TextButton(
                onPressed: () {
                  UpdateService.completeFlexibleUpdate();
                },
                child: Text("Finish Update"),
              )
            ]
          ),
        );

      }
    } catch (e) {
      //The user choosing to ignore the flexible update should trigger an exception 
      //Your preferred method of showing the user erros
    }
  }

In a nutshell

  • We're showing the user a dialog to tell the user there is an update available and it can be downloaded asynchronously
  • If the user ignores the dialog, it will fire an exception so you'll know how to handle
  • The user can also choose to go ahead, in this case when the update is downloaded, the user will be shown a dialog telling them to complete the process
  • The user can click "Finish Update" to complete the process or the update will happen automatically when next the user fires up the application

Testing

Now that we've figured out how to implement this two types of updates, we have to test them. Now, as you've probably deduced, testing features like this will be tricky and they are. Let's get to it.

Internal App Sharing

Internal app sharing is used to quickly share app bundles using links within your internal team. You can find out more here. In order to test this feature, you need to upload release versions of your application to your internal app sharing platform. These app bundles must have a difference in version codes i.e the version code of one app bundle must be greater than the order.

The internal app sharing console can be located with this link. Below is a screenshot of the console:

To put this in a clearer way, let me itemize the nuances to be followed:

  • First of, you need to create release versions, I personally recommend app bundles. You can see details here. Make sure all your releases are signed with the same keystore
  • You'll need to generate to signed APKs/App bundles to test it. One APK/App bundle will have a version code greater than the other. For example APK 1 will have the version code: 1 and APK 2 will have the version code: 2.

:::info :bulb: You can set the version code by changing the corresponding values in local.properties by editing the flutter.versionCode field.

:::

  • Upload both generated APKs/App bundles to your internal app sharing console and they should appear as such:
  • Next, you should download the version of the older version via the link in the console.
  • Once you've installed the older version via the link generated from the console, tap the link for the newer version from the console, BUT DO NOT INSTALL THE UPDATE. You should see that an update is availabe.
  • Open the already-installed older verson and you should see your implemented update type working : )

Following these laid-out items should help you properly test the feature. It might take a while for feature to actually work in production, so be patient.

Conclusion

Hopefully, you have been able to learn something about this feature. Other features can be implemented like deciding which kind of features should be immediately updates and which could be flexble and much more. Please feel free to email me at if you have any questions. Well then, till our next rodeo!