Dynamic Feature Module Integration Android

Shubham Agrawal
The Startup
Published in
6 min readOct 18, 2020

--

OnDemand Dynamic Delivery
Dynamic Feature (On-Demand Delivery)

The size of the application has always been an extreme pain for the developers. Sometimes the large libraries make the app size even larger.

To cope up with that Google released On-Demand Delivery Module. Through this feature, you can download and install important components later on as a feature module.
For example, consider an app that has a chat feature that only a few users use. It may make sense to add that feature as a downloadable feature rather than including it all for the first time installation of the app which will make your app size lighter the first time install.

Dynamic Delivery Usage

So, let's go through each step to let you configure the on-demand feature module. You can configure it through Android Studio in simple easy steps.

STEP 1: Click on File > New > New Module from Menu.

STEP 2: Select Dynamic Feature Module and Click Next.

STEP 3: Enter your Module name and enter your Package name as com.example.project.featurename. and Click Next.

STEP 4: Inside Module Download Options, enter your Module Title and leave the rest of the configured options as default as was set by Android Studio and Click Finish.

STEP 5: Once you finish, the Android Studio will do its work and Gradle will sync.

STEP 6: Once Gradle will finish its syncing, you will see your feature module on the left panel below your app module as a new project with its own Manifest, Java, and res folder.

STEP 7: Implement Google Play Core dependencies inside your app module using API. You can use “api” for Google Dependencies to use it inside your feature module. “api” acts as a common dependency to be used in both modules.

api 'com.google.android.play:core:1.8.2'

STEP 8: Now, inside your app manifest, add this line if you don’t have an application class.

<application android:name="com.google.android.play.core.splitcompat.SplitCompatApplication"
...
...
</application>

OR, If you have an existing Application class, then extend that class with SplitCompatApplication and override attachBaseContext() method and add the line I have added in the sample file.

package com.example.app;

import com.google.android.play.core.splitcompat.SplitCompatApplication;

public class App extends SplitCompatApplication {

@Override
public void onCreate() {
super.onCreate();
}

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
SplitCompat.install(this);
}
}

STEP 9: Now download the feature module using the following sample below.

package com.example.app;

import androidx.appcompat.app.AppCompatActivity;
import com.google.android.play.core.splitinstall.SplitInstallManager;
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
import com.google.android.play.core.splitinstall.SplitInstallRequest;
import com.google.android.play.core.splitinstall.SplitInstallSessionState;
import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener;
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;


public class MainActivity extends AppCompatActivity {


private SplitInstallManager splitInstallManager;
private int mySessionID;
SplitInstallStateUpdatedListener splitInstallStateUpdatedListener =
state -> {
if (state.sessionId() == mySessionID) {
switch (state.status()) {
case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION:
// Large module that has size greater than 10 MB requires user permission
try {
splitInstallManager.startConfirmationDialogForResult(state,
DashboardActivity.this, 110);
} catch (IntentSender.SendIntentException ex) {
// Request failed
}
break;

case SplitInstallSessionStatus.DOWNLOADING:
Log.i(TAG, "Downloading");
// The module is being downloaded
int totalBytes = state.totalBytesToDownload();
int progress = state.bytesDownloaded();
break;

case SplitInstallSessionStatus.INSTALLING:
Log.i(TAG, "Installing");
// The downloaded module is being installed
break;

case SplitInstallSessionStatus.DOWNLOADED:
Log.i(TAG, "Downloaded");
Toast.makeText(this, "Module Downloaded", Toast.LENGTH_SHORT).show();
// The module download is complete
break;

case SplitInstallSessionStatus.INSTALLED:
Log.i(TAG, "Installed");
// Use the below line to call your feature module activity
Toast.makeText(this, "Module Installed", Toast.LENGTH_SHORT).show();

Intent intent = new Intent();
intent.setClassName("com.example.app", "com.example.app.feature.AnotherActivity");
intent.putExtra("id", "12354");
startActivity(intent);
// The module has been installed successfully
break;

case SplitInstallSessionStatus.CANCELED:
Log.i(TAG, "Canceled");
// The user cancelled the download
break;

case SplitInstallSessionStatus.PENDING:
Log.i(TAG, "Pending");
// The installation is deferred
break;

case SplitInstallSessionStatus.FAILED:
Log.i(TAG, "Failed");
// The installation failed
}
}
};


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);


splitInstallManager = SplitInstallManagerFactory.create(this);
splitInstallManager.registerListener(splitInstallStateUpdatedListener);

Button btnDownload = findViewById(R.id.btnDownload);
btnDownload.setOnClickListener(v -> onClickDownloadFeatureModule()); // Using JAVA_8


}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
Log.d(TAG, "onActivityResult: ");
if (requestCode == 110) {
if (resultCode == RESULT_OK) {
Log.i(TAG, "onActivityResult: Install Approved ");
}
}
super.onActivityResult(requestCode, resultCode, data);
}




public void onClickDownloadFeatureModule() {
if (!splitInstallManager.getInstalledModules().contains("pdffeature")) {
SplitInstallRequest splitInstallRequest = SplitInstallRequest.newBuilder()
.addModule("feature")
.build();
splitInstallManager.startInstall(splitInstallRequest)
.addOnSuccessListener(result -> this.mySessionID = result)
.addOnFailureListener(e -> Log.i(TAG, "installManager: " + e.toString()));
} else {
Intent intent = new Intent();
intent.setClassName("com.example.app", "com.example.app.feature.AnotherActivity");
intent.putExtra("id", "12354");
startActivity(intent);
}
}

}

NOTE: If your feature module size is greater than 10MB, then a user confirmation dialog will show up to let the user know what they are downloading.

User Confirmation Dialog if size of the feature module is greater than 10 MB
User Confirmation Dialog for size >10 MB

In the above example, once the feature module is downloaded and installed, you can call the feature module activity using Intent.

Intent intent = new Intent();
intent.setClassName("com.example.app", "com.example.app.feature.AnotherActivity");
intent.putExtra("id", "12354");
startActivity(intent);

STEP 10: Now inside your feature module java folder, create an activity as you do in a normal app module and call it using the above Intent calls. Inside that activity override attachBaseContext()

@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
SplitCompat.install(this);
}

That’s it… You are done with the on-demand feature module android implementation.

Now, the testing part of it.

a) Test using an internal test track

Upload your app to the internal test track and install it on a device with a user account that has access to the internal test track. When using an internal test track, the following conditions must be met:

  1. The user account is part of the Internal Test Track.
  2. The user account is the primary account and it’s selected in the Play Store.
  3. The user account has downloaded the app from the Play Store (the app is listed in the user’s Google Play library).
  4. The user account does not currently have a review for the app.

After the account on the device has downloaded the app at least once from the internal test track and is part of the testers list, you can deploy new versions of the app locally to that device (for example, using Android Studio).

b) Test using an internal sharing app

Alternatively, for rapid iteration, you can use internal app sharing to test your integration. This method lets you quickly test changes by skipping some of the verification that happens with other test tracks.

FEW IMPORTANT THINGS TO REMEMBER

  1. You may face multiple Gradle errors, while you are configuring your feature module dependencies. You may find answers to a few of them on Stack Overflow easily. For ease, I am adding my feature module build.gradle file.
apply plugin: "com.android.dynamic-feature"

android {
compileSdkVersion 29
buildToolsVersion "30.0.2"

defaultConfig {
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
flavorDimensions "environment"
}

productFlavors {
prod {
dimension "environment"
}
qa {
versionNameSuffix "-qa"
dimension "environment"
}
}

compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}

}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation project(":app")
implementation 'com.mindorks.android:prdownloader:0.6.0'

}

The above file will help you if you have multiple flavors in your app.

2. The feature module can access all your app module files, but not vice-versa. Since your feature module will be downloaded later that’s why the app module doesn’t have the information on how to handle them which then will lead to a crash.

Module 2 and Module 3 are partially accessed just to open them, but the module can access all the app contents.

3. This feature will only work in a bundled app.

4. Caution: You have to set shrinkResources flag to false in order to proceed with DFM, which is really a pain.

Done! Happy Coding 😁

--

--