Part 1: Dynamic Delivery in multi-module projects at Bumble

Yury
Bumble Tech

--

Dynamic Delivery is a technology which allows you to install and delete parts of an application while the application is working, in order to reduce the space it occupied. If there are functions not being used, what is the point of a user having them on their device?

In part 1 of this article, I explore Dynamic Delivery and its API in more detail, specifically how to download and remove modules. In part 2, based on an example, I will unpack how I used Dynamic Delivery in our application and reduced the size of the application, saving half a megabyte of space.

So, let’s get started!

Modules for Dynamic Delivery

Functions which a user does not need, and which can be deleted, can be said to include the following:

  1. functions relating to А/B tests and user groups. Some functions may only be accessible in certain regions and, without Dynamic Delivery, on all other users’ devices, they are just dead weight.
  2. specific functions which not all users need. A classic example would be a module with a camera for recognising a bank card number.
  3. functions that are no longer accessible once a user performs certain actions. These might, for example, be registration screens which can be deleted after registration has been completed, and can be re-installed later if the user decides to register another account.

These kinds of functions can easily be moved to separate downloadable modules to reduce the space the application occupies. If this module is not installed at the same time as the application, then the size of the application, as displayed in Google Play, will be reduced accordingly. Smaller application size translates into a greater number of installations. It is also important to remove unused modules in order for your application to take up less space. If a device is running out of space, Google Play gives the user the option to delete some applications, and it also sorts applications according to the space they occupy. And you don’t want to be at the top of that list.

Modules with the functions listed above may contain code and any type of resources. Following installation, classes get loaded into a ClassLoader and may be used. You will be able to access the resources from the installed module. However, there is a ‘but’…

Dynamic Feature Module

You can only work with the downloaded code using reflection. This ensures that the dynamically downloadable modules are secure to use. If the downloadable module was connected using the compileOnly dependency, then, when attempting to use this module’s classes, if this module has not been installed,

ClassNotFoundException would be returned. In such case, reflection allows you to:

  1. handle situations more securely, when the required classes are not to be found in runtime.
  2. avoid accidental use of classes from the Dynamic Feature Module, not checking to see whether they are in the ClassLoader.

This is what it looks like from the point of view of module structure in Gradle:

Source of picture: Patterns for accessing code from Dynamic Feature Modules, recommended reading

The modules in the first row (:about and others) are Dynamic Feature Modules. They depend on the application’s basic module and are able to use its code and resources easily. The application module is in the second row and its dependencies are in the third.

It might seem that the requirement to use reflection nullifies all the advantages of the technology, but I will show you how, in the case of our multi-module approach, all the reflection can be reduced to just two or three calls.

SplitInstallManager

To start with, let’s see how to install modules. For this, we use SplitInstallManager, which is part of the com.google.android.play:core library. You may already be familiar with this from In-app Updates and MissingSplitsManager.

Here is how to work with modules:

  1. using SplitInstallManager.installedModules verify that we do not already have the module installed.
  2. if the module is not installed, request installation using SplitInstallRequest, specifying its name.
  3. track the progress of the installation process; show the user the modal UI for download, if the user is waiting.
  4. if the module has been successfully installed, start to use it via reflection. If an error has occurred, show it.

It’s all quite simple and self-evident, with the exception of one not very convenient API, which I will show you using the example of code from android.developers.com.

Verifying the module has been installed

Request installation

splitInstallManager.startInstall will return Task<Int>, but not one from the com.google.android.gms:play-services-tasks package, to which in your case the Kotlin Extensions have most likely already been written, but rather its own one. Their API coincides entirely, but the package names are different. An installation session identifier is returned in addOnSuccessListener callback. What’s more, the same one is returned if you request installation several times over, so don’t be afraid to do so. There is only one limitation: if you specify downloading several modules at once via SplitInstallRequest.addModule(…).addModule(…), then when you attempt to request installation of just one of them on a second or subsequent occasion, it will return the error INCOMPATIBLE_WITH_EXISTING_SESSION. If the error occurred before the session was accepted, or during installation, an error will be returned in addOnFailureListener.

Progress of installation

Installation state updates will be available in SplitInstallStateUpdatedListener. Updates of all sessions are available here, and we need to filter them ourselves based on the session identifier. In SplitInstallSessionState the following are available to us:

  1. current installation state (download, unpacking, installation etc.).
  2. the number of bytes downloaded and how many still remain (this information can be used to show installation progress).
  3. error code (this same error code appear at addOnFailureListener).

Confirmation from user

Whenever the size of the module exceeds 10 MB you need to ask the user for confirmation of download. In SplitInstallSessionState a special state, REQUIRES_USER_CONFIRMATION, is returned. This will be the state of installation until you call splitInstallManager.startConfirmationDialogForResult(Activity, SplitInstallSessionState, Int). This call will run Google Play via startActivityForResult with an installation confirmation dialogue. If the user clicks on the ‘download’ button, then installation will continue, and you don’t need to do anything. If they click on the ‘cancel’ button, then installation will end with a CANCELED state.

Installation

To support the load of classes and resources in an application, you need to use SplitCompat.

The application version will extract classes.dex from the downloaded APK files and will load them into ClassLoader, and will also call context.getAssets().addAssetPath(String) with the downloaded module’s APK file. The Activity version will just add the path to the AssetManager. It might seem unnecessary to call installActivity if you are using the application’s Context, but not doing so can cause problems with the configuration of Activity, which, in that case, will be ignored.

Deferred installation

You can request Google Play Services to install a module at some point in the future. The official documentation describes the time “at some point in the future” as “best-effort when the app is in the background”. In practice, the module will be downloaded when your application is not running and when Google Play is installing updates for your application or others.

Requesting installation in the background is very simple:

At the same time, there is no way for you to track it, for the simple reason that it won’t run as long as your application is running.

Despite this limitation, this approach may well still be useful. For example, for functions relating to an A/B test. If you have positioned the entry point to an application’s new screen somewhere that is visible, then the user might click on it — at the very least out of curiosity. Then why not request installation of such modules in the background, so the user won’t have to wait at a later point in time?

Pulling it all together

For a start, let’s write a function to verify whether the module, moduleName, has been installed and whether the required class, className, can be used via reflection.

In the case of non-release builds the modules installed will be empty. So, we add verification as to whether or not there is a class — which we intend to use via reflection. We use exactly this reload of Class.forName in order not to initialise static fields of the class and perform work that could be left until later.

During the installation process we have to handle various module installation states. For this, we use a simple sealed class.

Unfortunately, we cannot entirely go away from using SplitInstallSessionState in RequiresConfirmation, since it is essential for calling splitInstallManager.startConfirmationDialogForResult. However, we only seem to need a session identifier. I hope this will change in the future.

We also need a function that will download a given module and track progress. At Bumble we use a reactive approach, so we will return Observable<DynamicDeliveryProgress>. Once the module is installed, we will complete observable.

From the UI side don’t forget to handle the DynamicDeliveryProgress.RequiresConfirmation state. Once the installation is completed you will need to call SplitCompat.installActivity(this) again, in order to download resources from downloaded APK files.

In this implementation we don’t deal with the INCOMPATIBLE_WITH_EXISTING_SESSION state at all, since, in our case, all the modules are installed one-by-one. However, this error is quite easy to deal with. When it occurs in retryWhen you can create an Observable which:

  1. again subscribes to state updates via splitInstallManager.registerListener.
  2. finds a session in splitInstallManager.getSessionStates in which requested modules are being installed.
  3. waits for installation to end and sends a request for load(…) to be called again.

Conclusion

Dynamic Delivery from Google lets you download and delete modules while the application is working. This is an excellent way of saving space on a device: as a rule, there are modules in an application which are rarely used, but if necessary they can be loaded while the application is working.

Despite the not-so-convenient API it is entirely possible to hide all the module download operations behind a single interface. It is easy to execute a request for downloading Dynamic Delivery modules, but you have to be careful when processing it’s dozens of different states.

There are two other points which merit pointing out:

  1. once the module is installed, don’t forget to notify the current Activity and so ensure that it has access to the resources downloaded.
  2. don’t forget to request confirmation from the user, if necessary, by running the special dialogue Activity.

In part 2 of the article, I will tell you how I used Dynamic Delivery for one of Bumble’s projects.

--

--