Blog Infos
Author
Published
Topics
,
Published

Understanding the problem, code implementation, testing & troubleshooting

Introduction

Dynamic Feature Modules (DFMs) in Android development are a powerful tool for modularising your app, reducing size, and enabling on-demand delivery. In this comprehensive guide, we’ll walk through everything from setting up your first DFM to solving common issues that developers often face.

This blog post was first published on my personal blog, HelloHasan.Com.

Full Source Code — GitHub.Com

Problem Statement

We know that, on average, 80% of users use only 20% of the features in an application. Suppose you have an application with 30 features (20 MB), and all of your features are in the app module. So, every user will download a 20 MB app, and it will occupy the required storage after installation. However, most of the features are not used by a user. For a specific user, it’s a waste of memory because they may only need 20–30% of the features.

Here we have only one module. Name: app

 

What would it be like if we could download only the parts of our app that we needed? Consider an eCommerce application with a customer support feature that includes chat and audio call capabilities. To implement this feature, you have used a third-party SDK (e.g., 5 MB).

You know that this feature is not necessary for every user. So, the idea is to exclude the customer support feature from your main app module. When a user taps on the customer support button, they will be able to download this feature from the Play Store. After downloading, the user can use the feature.

Understanding Dynamic Feature Module

Generally, when we start Android development, we work in a monolithic app module. There is only one module in our application, and all features remain in this app module. We upload the .apk or .aab file to the Play Store, and users download our entire application at once.

On the other hand, in a dynamic feature module, we can split our application into a few modules. The user will download the base module (app module) for the first time. After that, they will be able to download other feature modules at runtime.

Example

Suppose you have an eCommerce application. Another feature is customer support (chat & audio-video call). Your main feature is online shopping (app module, for example, its size is 10 MB). However, for some special cases, users need to chat or call customer support. So, you can develop your customer support feature as a dynamic feature module outside the app module (for example, the customer support feature size is 5 MB).

Wireframe of Android on Demand dynamic feature module

The first time a user downloads the app, they will get the app module without the customer support feature. So, they will download a 10 MB app. When someone needs to contact customer support, they will click on your specific button and download the customer support module (size 5 MB) from the Play Store on-demand basis. This way, lots of users save an additional 5 MB cost.

That’s the idea of On-Demand Dynamic Feature Delivery Module!

Setup Dynamic Feature Module

First of all, I have created a new project with the App ID com.hellohasan.hasanerrafkhata. This app is already on the Play Store, and I have developer access on the Google Play Console. The App ID is important here for testing the dynamic feature module. You need to upload the .aab file to the Google Play Store or use Google Play Internal App Sharing. So, make sure you have access to the Play Store for your mentioned Application ID.

After creating the project with my own app ID, I added dependencies in the build.gradle.kts. In some tutorials, I found instructions to add the Google Play Core library. However, I believe it’s enough to add the feature-delivery library for this case.

build.gradle.kts (app module)
dependencies {
    // other dependencies

    implementation("com.google.android.play:feature-delivery:2.0.0")
    implementation("com.google.android.play:feature-delivery-ktx:2.0.0")
}

After syncing the project, I encountered the first error!

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:checkDebugDuplicateClasses'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable
   > Duplicate class android.support.v4.app.INotificationSideChannel found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.app.INotificationSideChannel$Stub found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.app.INotificationSideChannel$Stub$Proxy found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.os.IResultReceiver found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.os.IResultReceiver$Stub found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.os.IResultReceiver$Stub$Proxy found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.os.ResultReceiver found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.os.ResultReceiver$1 found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.os.ResultReceiver$MyResultReceiver found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     Duplicate class android.support.v4.os.ResultReceiver$MyRunnable found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-26.1.0-runtime (com.android.support:support-compat:26.1.0)
     
     Go to the documentation to learn how to <a href="d.android.com/r/tools/classpath-sync-errors">Fix dependency resolution errors</a>.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 9s

To solve this error, I added these two lines in the gradle.properties file.

android.useAndroidX=true
android.enableJetifier=true
Create Dynamic Feature Module

Create a new module like below:

Creating Dynamic Feature module in Android

 

Confirmation of creating new dynamic feature module with required configuration

 

In the above configuration, I have set ‘Do not include module at Install-time (on-demand only)’ because I want to install the dynamic module on demand only. So, my dynamic module will not be included at install time. After creating the translation dynamic module, my project structure looks like the one below:

Now there are two modules in our project. app & translationdynamicmodule

 

Then, I created a new Activity, CustomerSupportActivity.kt, inside the customer_support module. Android Studio internally changed some configurations in a few different places. If you want to convert any existing feature module into an on-demand dynamic module, you have to configure these manually. Let’s check them!

Changes in build.gradle.kts (app level)
android {
    // other configurations

    // this line is automatically added by Android Studio 
    // after creating a new dynamic module
    dynamicFeatures += setOf(":customer_support") 
}
AndroidManifest.xml (dynamic module)
<dist:module
    dist:instant="false"
    dist:title="@string/title_customer_support">
    <dist:delivery>
       <dist:on-demand />
    </dist:delivery>

    <dist:fusing dist:include="false" />
</dist:module>
build.gradle.kts (dynamic module)
plugins {
    id("com.android.dynamic-feature")
    id("org.jetbrains.kotlin.android")
}

// others configurations

dependencies {
    implementation(project(":app"))
    
    // other dependencies
}
build.gradle.kts (Project level gradle)
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id("com.android.application") version "8.1.4" apply false
    id("org.jetbrains.kotlin.android") version "1.8.10" apply false
    id("com.android.dynamic-feature") version "8.1.4" apply false
}
settings.gradle.kts
include(":app")
include(":customer_support")
Implementation of Android Dynamic Feature Delivery
Create dynamic module Activity

My dynamic module is named ‘customer_support’ module. However, it’s not my objective to develop a real customer support chat or audio/video call feature in this sample project. For the sake of simplicity, I have included six large images (~18 MB) in CustomerSupportActivity.kt. Using these large images, we will clearly understand the effect of the dynamic module.

Our Sample dynamic module app UI design

 

MainActivity.kt (app module)

In my app module, I have only one activity — MainActivity.kt. I simply added a banner image of the eCommerce app and then included a Customer Support button. When a user taps on the Customer Support button for the first time, they will see an alert dialog to confirm downloading the dynamic module. Upon clicking the Download button, MainActivity.kt will start downloading the customer_support module from the Google Play Store. After downloading and installing, MainActivity.ktwill receive a few callbacks. Based on those callbacks, the activity will redirect the user to CustomerSupportActivity.kt.

Add a simple button in MainActivity to open the dynamic module Activity. After clicking the button, we will check if the dynamic module is already downloaded. If yes, then open the dynamic module Activity. Otherwise, we will request to download the dynamic module using our utility class.

When our utility class tries to download and completes the process, successfully installing the new module, our app module’s MainActivity will be notified by a callback.

class MainActivity : ComponentActivity(), DynamicModuleListener {
private val CUSTOMER_SUPPORT_DYNAMIC_MODULE = "customer_support"
private lateinit var dynamicModuleDownloadUtil: DynamicModuleDownloadUtil
private var logState = mutableStateOf("Activity Log:\n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
dynamicModuleDownloadUtil = DynamicModuleDownloadUtil(baseContext, this)
// ui codes
}
override fun onDownloading() {
logState.value += "${getCurrentTimestamp()}: Downloading...\n"
}
override fun onDownloadCompleted() {
logState.value += "${getCurrentTimestamp()}: Module download completed.\n"
}
override fun onInstallSuccess() {
logState.value += "${getCurrentTimestamp()}: Module install Success!\n"
startCustomerSupportActivity()
}
override fun onFailed() {
logState.value += "${getCurrentTimestamp()}: Module download or installation failed.\n"
}
private fun openCustomerSupportFeature() {
if (dynamicModuleDownloadUtil.isModuleDownloaded(CUSTOMER_SUPPORT_DYNAMIC_MODULE)) {
logState.value += "${getCurrentTimestamp()}: Module is already downloaded.\n"
startCustomerSupportActivity()
} else {
dialogState.value = true
}
}
private fun startCustomerSupportActivity() {
val intent = Intent()
intent.setClassName(
"com.hellohasan.hasanerrafkhata",
"com.hellohasan.customer_support.CustomerSupportActivity"
)
startActivity(intent)
}
}
DynamicModuleDownloadUtil.kt

This util file is responsible to handle initialization, checking if a module has already been downloaded and also starting of a dynamic module download with all the possible callbacks. This can be a good idea to have to handle multiple dynamic modules in your app.

const val TAG = "dynamic_module_util"
interface DynamicDeliveryCallback {
fun onDownloading()
fun onDownloadCompleted()
fun onInstallSuccess()
fun onFailed(errorMessage: String)
}
class DynamicModuleDownloadUtil(context: Context, private val callback: DynamicDeliveryCallback) {
private lateinit var splitInstallManager: SplitInstallManager
private var mySessionId = 0
init {
if (!::splitInstallManager.isInitialized) {
splitInstallManager = SplitInstallManagerFactory.create(context)
}
}
fun isModuleDownloaded(moduleName: String): Boolean {
return splitInstallManager.installedModules.contains(moduleName)
}
fun downloadDynamicModule(moduleName: String) {
val request = SplitInstallRequest.newBuilder()
.addModule(moduleName)
.build()
val listener = SplitInstallStateUpdatedListener { state -> handleInstallStates(state) }
splitInstallManager.registerListener(listener)
splitInstallManager.startInstall(request)
.addOnSuccessListener { sessionId ->
mySessionId = sessionId
}
.addOnFailureListener { e ->
Log.d(TAG, "Exception: $e")
handleInstallFailure((e as SplitInstallException).errorCode)
}
splitInstallManager.unregisterListener(listener)
}
private fun handleInstallFailure(errorCode: Int) {
when (errorCode) {
SplitInstallErrorCode.NETWORK_ERROR -> {
callback.onFailed("No internet found")
}
SplitInstallErrorCode.MODULE_UNAVAILABLE -> {
callback.onFailed("Module unavailable")
}
SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED -> {
callback.onFailed("Active session limit exceeded")
}
SplitInstallErrorCode.INSUFFICIENT_STORAGE -> {
callback.onFailed("Insufficient storage")
}
SplitInstallErrorCode.PLAY_STORE_NOT_FOUND -> {
callback.onFailed("Google Play Store Not Found!")
}
else -> {
callback.onFailed("Something went wrong! Try again later")
}
}
}
private fun handleInstallStates(state: SplitInstallSessionState) {
if (state.sessionId() == mySessionId) {
when (state.status()) {
SplitInstallSessionStatus.DOWNLOADING -> {
callback.onDownloading()
}
SplitInstallSessionStatus.DOWNLOADED -> {
callback.onDownloadCompleted()
}
SplitInstallSessionStatus.INSTALLED -> {
Log.d(TAG, "Dynamic Module downloaded")
callback.onInstallSuccess()
}
SplitInstallSessionStatus.FAILED -> {
callback.onFailed("Installation failed")
}
SplitInstallSessionStatus.CANCELED -> {
callback.onFailed("Installation Cancelled")
}
}
}
}
}
Full Source code of the project

There is another Kotlin file for the alert dialog composable widget. You can easily understand the full flow after exploring the entire project. Please check here for the full source code:

GitHub — hasancse91/dynamic-feature-module-android: Android Dynamic On Demand Feature Delivery…

Android Dynamic On Demand Feature Delivery Module Implementation using Kotlin — GitHub …

github.com

Dynamic Delivery Testing, troubleshooting & some life saving tips
Build the project

I’ve previously mentioned that the customer support module contains six large images (18 MB), resulting in a final .aab file size of 22.5 MB.

Analyse .aab file with base module and customer_support module

So, the size of my main app (base module) is 4.8 MB. After installing this app, you will find a button for downloading the dynamic module. You can then download the dynamic module, which has a size of 17.6 MB, from the Play Store.

Android Dynamic Feature Module Testing

Upload your .aab file to Google Play Internal App Sharing and use the URL to install the app from play store.

Internal App Sharing of Google Play Console
Demo Video of Android Dynamic Feature Module Testing

Some life saving tips & troubleshooting
  1. Make sure the versions of your common dependencies are the same across modules. You might need the same dependencies in the app module as well as dynamic modules. All common library versions should be the same. Otherwise, you may face unwanted build errors.
  2. Ensure that your Java and Kotlin versions are consistent across both the app and other modules.
  3. Check the compatible Kotlin version of the dynamic feature dependency. If your project’s Kotlin version is not compatible with the latest dynamic feature library, consider downgrading the library version (if you are unable to update the Kotlin version immediately).
  4. If any of your third-party libraries or SDKs has NDK configuration internally, you may need to add a few configurations in the app module’s gradle file (under android). I believe this is not directly related to the dynamic feature module. In one of my projects, I had to add this configuration in the gradle file due to a third-party SDK.
ndk {
    abiFilters.add("arm64-v8a")
    abiFilters.add("x86_64")
    abiFilters.add("x86")
    abiFilters.add("armeabi-v7a")
}

5. If your modules contain duplicate classes, you may encounter errors during the release APK build. I have faced this type of error:

java.lang.RuntimeException: Duplicate class org.intellij.lang.annotations found in modules …

To fix the error I had to exclude the duplicate module from the project:

// build.gradle.kts (dynamic module)
// this configuration will exclude the duplicate classes from my dynamic module
configurations.implementation {
    exclude(group = "org.jetbrains", module = "annotations")
}

6. If your app module contains flavors in the build.gradle file, ensure that you add those flavor names to your dynamic module’s build.gradle.kts file.

// build.gradle.kts (dynamic module)
android {
  ...
  flavorDimensions += "version"
  productFlavors {
      create("dev") {}
  
      create("stage") {}
  
      create("live") {}
  }
}

There were three product flavors in my app module (dev, stage, live). That’s why I added these three flavors to my dynamic module.

That’s it! I hope you enjoyed the blogs. Feel free to share your feedback in the comment section. Don’t forget to clap! 👏 👏 👏

Let’s connect on LinkedIn.

This article was previously published on proandroiddev.com

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

No results found.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
The ModalBottomSheet in Jetpack Compose is easy to use, it just pops up at…
READ MORE
blog
Discussions about accessibility, especially in software development, often center around screen reader accessibility. With…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu