In this post I am going to show you how to solve a common problem that arises when using dynamic feature modules in your project: code defined inside them is inaccessible to your base application during compilation… unless you use reflection 😱.
Just the mention of reflection is enough to scare some developers away, but there are a few elegant solutions to safely access what you need using at most one reflection call… and a clever optimization technique to get that number down to zero!
Read on to learn about different approaches you can take: ServiceLoaders, reflection, and how it can work with Dagger2 in a sample modularized app, available on GitHub.
Why dynamic feature modules?
Last year, we launched Android App Bundles and Google Play’s Dynamic Delivery to help developers reduce app size and streamline the release process. Find out more about the benefits of app bundles.
But switching to app bundles is just the first step. The new format opens up new possibilities for modularizing your code base to deliver an even more customized experience to users.
For on-demand modules, you use the Play Core API to install new code and resources when the user navigates to an optional feature in your app; and for conditional modules, their availability will be determined at install time by the user’s device configuration, such as hardware and software features, user’s country or the Android OS version.
What these new, customizable types of modules have in common, is that they both use the
com.android.dynamic-feature Gradle plugin. We’re going to call them dynamic feature modules, or DFMs. When you distribute your app as an Android App Bundle, Play Store will deliver these modules to user’s devices as separate APKs at the appropriate time.
From libraries to dynamic feature modules
Migrating some of your library modules to DFMs requires an important change in your project structure, that is inverting the dependency between the base
com.android.application and any dynamic features.
In the standard project structure that uses libraries, the base
com.android.application module depends on
com.android.library modules, which means you can use any classes defined in the library from the base module freely.
With DFMs however, the base
com.android.application is a dependency of
com.android.dynamic-feature modules, which means you can use any classes defined in the base module and its libraries in the DFM, but you can’t reference any code defined in the DFM from the base application at compile time.
Once the APKs produced by the DFM are installed with your application (regardless of the delivery mechanism: install-time, on-demand or conditional), their code is available to the ClassLoader at runtime.
Remember to always use SplitCompat from Play Core Library when you have on-demand modules.
There are a couple of obvious solutions to this problem, depending on your use case. If all you need from your DFM is to launch an Activity, you can simply pass its name (as a
String; you can’t use
.class notation, because the class isn’t accessible at compile time) to
This is the approach that we used in the dynamic features sample. You can also check it out if you want to learn how to use the Play Core Library to install on-demand modules.
Contribute to googlesamples/android-dynamic-features development by creating an account on GitHub.
In the following sections I want to show you how to load a custom class from a DFM (after it has been installed) and access it in the base module, using three approaches. You can find the full code for the example used in this article in the dynamic code loading sample.
Android dynamic code loading sample for Dynamic Feature Modules. - googlesamples/android-dynamic-code-loading
In the sample code that we’ll be looking at, the app contains a simple counter that the user can increment by pressing a button. The counter resets every time the app is relaunched, unless the user installs an on-demand module that contains code which lets the app save the state of the counter to storage.
Of course that’s not something that you’d do in a real app, but it gives us a simplified scenario to work with, that you can adapt to your own dynamically loaded code.
The sample code is organized such that common parts are found in the main source set, but each approach for accessing code dynamically is implemented in its own variant.
The base module, “app”, contains these shared classes:
- MainActivity contains the UI,
- AbstractMainViewModel contains the logic, but needs to be subclassed in each build variant to provide
abstract fun initializeStorageFeature()
You can switch between the three approaches using the Build Variant switcher in Android Studio:
storage module is a dynamic feature module configured for on-demand delivery. What we’ll be looking at in this article is how you can access a class that lives in the
storage module from the base
Splitting the interface from the implementation
To begin, we need to create an interface for our dynamic feature code. We define it in the
app module, so that it can be used to directly call any functions implemented by the feature.
The interface for my storage feature is simple: it needs to be able to store and retrieve an integer.
Because we leave the implementation of that interface to be defined in the DFM module, the implementer is free to choose what their constructor will look like. Our storage feature will need a
Context, and might even request some other dependencies (like a
Logger for example), and the implementation might look something like this:
To make it simple to obtain an instance of this concrete
StorageFeature implementation and leave the instantiation to the implementer, we will also define a storage feature
Provider interface with a single
get() method that, given the necessary dependencies, will return a ready to use
Again, we will put the interface in the base module, while the DFM will contain the implementation.
You can check out the interface definition here, with a small difference: I’m using another interface to hold the feature’s
Dependencies, which will be useful for my Dagger setup later on, but is not really required otherwise.
Approach 1: Direct reflection call
The most straightforward method to obtain the
StorageFeature instance in our base app module is a single reflection call. If you’re checking out the sample while reading this, switch to the
reflectDebug build variant.
Take a look at the first MainViewModel implementation in the
reflect source set:
Notice that I’m using the standard
Class.forName() to get the class handle of the
Provider, but then Kotlin gives us a very nice way to get its singleton instance — objectInstance. This works because my Provider is declared as a Kotlin
If you’re not using Kotlin or don’t want to have an
object, you can revert to the regular
Once you have a reference to the
Provider, you no longer need any more reflect calls, you simply call
get() and continue calling your feature’s code through the
StorageFeature interface, with compile-time safety.
Approach 2: Leveraging the ServiceLoader
Our next approach: load an interface implementation using a
ServiceLoader instead of using reflection directly. This can be especially useful if you want to load multiple implementations of the same interface.
ServiceLoader comes with a serious performance impact on Android by default, which is why you shouldn’t use it unless you are on the newest versions of R8 (1.5.X branch, included in Android Gradle Plugin 3.5.0+) with code shrinking and optimizations enabled.
When working with
ServiceLoader and R8 optimizations, you get the benefit of getting rid of reflection entirely in the final byte code.
ServiceLoader is fairly straightforward. Here’s our MainViewModel implementation (remember to switch to the
serviceLoaderRelease build variant in Studio):
To get the
ServiceLoader.load() and pass in the interface that you’re looking for as the first argument.
There are three conditions you must meet to enable the R8 optimization:
- You must call the two-argument version of
- Both arguments must use class constants (
.classin Java or
- You must not call any methods on the returned
You can iterate on the returned
ServiceLoader to get instances of any implementations of the interface that have been found.
ServiceLoader knows where to look based on a file you have to put in the
META-INF folder in the DFM. The filename has to match the interface name, and the file contents is the name of the implementing class.
ServiceLoader instantiates the
Provider for you, so it has to have a no-argument constructor.
To verify that the R8 optimization worked, you can use the DEX Viewer in the APK Analyzer in Android Studio and search for references to the
ServiceLoaderclass in the base DEX file. When R8 worked correctly, you shouldn’t be able to find any.
If you can still see a call to
ServiceLoader.load()(as in the animation above) this means the optimization did not work, and you’ll be doing disk I/O operations on the calling thread and possibly freeze your app, so if you choose to use the
ServiceLoaderpattern, you had better make sure that you are enabling the optimization correctly.
Approach 3: Integrating with Dagger2
The third approach shows how you can integrate with Dagger 2 to help instantiate the object graph on the DFM side. Check it out in the
daggerDebug build variant.
So far we’ve dealt with a very simple case, where the feature code only requires a
Context and then is able to create and return a
In reality, your dependencies can be a lot more complicated and using a dependency injection framework such as Dagger2 can assist you in making sure the right dependencies are instantiated and passed to the DFM.
Because of the way the Gradle module dependencies are set up, we can’t use a subcomponent of our base Dagger component in the DFM. However, we can use another mechanism called component dependencies to get the required dependencies from the parent component. You declare the dependency in the feature component:
And we’ll use the
StorageFeature.Provider implementation to instantiate the feature component and return the
Now whenever you try to compile your app, Dagger will tell you if there are any dependencies missing. For example, I’m making sure the base component has a
Logger object bound, so it can pass it on to the feature component as part of the
And finally, we have a custom
@Provides method in the base module that will take care of getting the
StorageFeature.Provider via reflection, passing in the required dependencies and caching the result:
This is of course one way of making Dagger work with the dynamic feature modules in your app, but a lot will depend on your current Dagger setup. Feel free to take the ideas from this sample or use whatever fits your current architecture.
For example, instead of caching the result in the Module, you can try using
dagger.Lazy<StorageFeature> at the injection site.
Whichever approach you choose, you have to make sure that you only instantiate the StorageFeature object graph after the DFM APK has been installed in your app.
If your DFMs are delivered conditionally or on-demand, remember to verify that first with
Modularization is a very broad topic that has implications on much more than simple class loading, such as the overall architecture of your app and even the final UX that end-users will see. In this article I focused on solving one specific problem: loading code from DFMs via reflection.
You can also check out the code changes that were made when applying techniques similar to those described in this article in a real app:
Splitting data sources for Search in different modules by florina-muntenescu · Pull Request #680 ·…
📢 Type of change Bugfix New feature Enhancement Refactoring 📜 Description Breaking data sources. Now each module…
Furthermore, Ben Weiss published a more general article that is a case study on modularizing Plaid a while ago:
A patchwork Plaid — Monolith to modularized app
How and why we modularized Plaid and what’s to come
The dynamic code loading sample that this article is based on uses the Play Core Library. In order to enable on-demand module delivery from Play, you should get familiar with the API and implement it correctly as stated in the linked documentation pages.
Remember to subscribe to our Android Developers publication for more stories about Android, Play, Kotlin and more!