An Android Studio Dynamic Feature Tutorial

Revision as of 18:53, 21 May 2019 by Neil (Talk | contribs)

Revision as of 18:53, 21 May 2019 by Neil (Talk | contribs)

With the basic concepts of Android Dynamic Delivery and Dynamic Features covered in the previous chapter, this chapter will put this theory into practice in the form of an example project. The app created in this chapter will consist of two activities, the first of which will serve as the base module for the app while the second will be designated as a dynamic feature to be downloaded on demand from within the running app. This tutorial will include steps to create a dynamic module from within Android Studio, upload the app bundle to the Google Play Store for testing, and use the Play Core Library to download and manage dynamic features. The chapter will also explore the use of deferred dynamic feature installation.

Creating the DynamicFeature Project

Select the Start a new Android Studio project quick start option from the welcome screen and, within the resulting new project dialog, choose the Empty Activity template before clicking on the Next button.

Enter DynamicFeature into the Name field and specify a package name that will uniquely identify your app within the Google Play ecosystem (for example com.<your company>.dynamicfeature) as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Java.

Adding Dynamic Feature Support to the Project

Before embarking on the implementation of the app, two changes need to be made to the project to add support for dynamic features. Since the project will be making extensive use of the Play Core Library, a directive needs to be added to the build configuration to include this library. Within Android Studio, open the app level build.gradle file (Gradle Scripts -> build.gradle (Module: app)), locate the dependencies section and add the Play Core Library as follows:

.
.
dependencies {
.
.
    implementation 'com.google.android.play:core:1.5.0'
.
.
}

Once a dynamic feature module has been downloaded, it is most likely that immediate access to code and resource assets that comprise the feature will be required by the user. By default, however, newly installed feature modules are not available to the rest of the app until the app is restarted. Fortunately, this shortcoming can be avoided by enabling the SplitCompat Library within the project. By far the easiest way to achieve this is to declare the application as subclassing SplitCompatApplication in the main AndroidManifest.xml file (also referred to as the base module manifest) as follows:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.ebookfrenzy.dynamicfeature">
<application
       android:name=
           "com.google.android.play.core.splitcompat.SplitCompatApplication"
       android:allowBackup="true"
       android:icon="@mipmap/ic_launcher"
       android:label="@string/app_name"
       android:roundIcon="@mipmap/ic_launcher_round"
.
.

Designing the Base Activity User Interface

At this point, the project consists of a single activity which will serve as the entry point for the base module of the app. This base module will be responsible for requesting, installing and managing the dynamic feature module.

To demonstrate the use of dynamic features, the base activity will consist of a series of buttons which will allow the dynamic feature module to be installed, launched and removed. The user interface will also include a TextView object to display status information relating to the dynamic feature module. With these requirements in mind, load the activity_main.xml layout file into the layout editor, delete the default TextView object, and implement the design so that it resembles Figure 87-1 below.


As 3.4 dynamic feature base ui.png


Figure 1-1

Once the objects have been positioned, select the TextView widget and use the Attributes tool window to set the id property to status_text. Begin applying layout constraints by selecting all of the objects within the layout, right-clicking the TextView object and choosing the Center -> Horizontally in Parent option from the menu, thereby constraining all four objects to the horizontal center of the screen.

With all the objects still selected, right-click on the TextView once again, this time selecting the Chains -> Create Vertical Chain menu option. Before continuing, extract all the button text strings to resources and configure the three buttons to call methods named launchFeature, installFeature and deleteFeature respectively.

Adding the Dynamic Feature Module

To add a dynamic feature module to the project, select the File -> New Module... menu option and, in the resulting dialog, choose the Dynamic Feature Module option as shown in Figure 87-2:


As 3.4 add new dynamic feature.png


Figure 1-2

With the Dynamic Feature Module option selected, click on the Next button and, in the configuration screen, name the module my_dynamic_feature and change the minimum API setting to API 26: Android 8.0 (Oreo) so that it matches the API level chosen for the base module:


As 3.4 configure dynamic feature.png


Figure 1-3

Click the Next button once more to configure the On-Demand options, making sure that both the Enable on-demand and Fusing options are enabled. In the Module Title field, enter text which reads “My Example Dynamic Feature”:


As 3.4 dynamic feature on demand options.png


Figure 1-4

Finally, click the Finish button to commit the changes and add the dynamic feature module to the project.

Reviewing the Dynamic Feature Module

Before proceeding to the next step, it is worth taking some time to review the changes that have been made to the project by Android Studio. Understanding these changes can be useful both when resolving problems and when converting existing app features to dynamic features.

Begin by referring to the Project tool window, where a new entry will have appeared representing the folder containing the new dynamic feature module:


As 3.4 new dynamic feature project window.png


Figure 1-5

Note that the feature has its own sub-structure including a manifest file and a package into which code and resources can be added. Open and review the AndroidManifest.xml file which should contain the property settings that were selected during the feature creation process including the on-demand, fusing and feature title values:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    package="com.ebookfrenzy.my_dynamic_feature">
    <dist:module
        dist:instant="false"
        dist:onDemand="true"
        dist:title="@string/title_my_dynamic_feature">
        <dist:fusing dist:include="true" />
    </dist:module>
</manifest>

A point of particular interest is that the module title string has been stored as a string resource instead of being directly entered in the manifest file. This is a prerequisite for the title string and the resource can be found and, if necessary, modified in the strings.xml file (app -> res -> values -> strings.xml) of the base module.

The build configuration for the dynamic feature module can be found in the Gradle Scripts -> build.gradle (Module: my_dynamic_feature) file and should read as follows:

apply plugin: 'com.android.dynamic-feature'
android {
    compileSdkVersion 28
 
    defaultConfig {
        minSdkVersion 26
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }
}
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':app')
}

The key entries in this file are the application of the com.android.dynamic-feature plugin which ensures that this module is treated as a dynamic feature, and the final implementation line indicating that this feature is dependent upon the base (app) module.

The build.gradle file for the base module (Gradle Scripts -> build.gradle (Module: app)) also contains a new entry listing the dynamic feature module:

apply plugin: 'com.android.application'
android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.ebookfrenzy.dynamicfeature"
        minSdkVersion 25
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
.
.
    }
    dynamicFeatures = [":my_dynamic_feature"]
}
dependencies {
.
.

Any additional dynamic features added to the project will also need to be referenced here, for example:

dynamicFeatures = [":my_dynamic_feature", ":my_second_feature", ...]

Adding the Dynamic Feature Activity

From the Project tool window illustrated in Figure 87-5 above, it is clear that the dynamic feature module contains little more than a manifest file at this point. The next step is to add an activity to the module so that the feature actually does something when launched from within the base module. Within the Project tool window, right-click on the package name located under my_dynamic_feature -> java and select the New -> Activity -> Empty Activity menu option. Name the activity MyFeatureActivity and enable the Generate Layout File option but leave the Launcher Activity option disabled before clicking on the Finish button.


As 3.4 dynamic feature add activity.png


Figure 1-6

Once the activity has been added to the module, edit the AndroidManifest.xml file and modify it to add an intent filter that will allow the activity to be launched by the base module (where <com.yourcompany> is replaced by the package name you chose in order to make your app unique on Google Play):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution"
    package="com.ebookfrenzy.my_dynamic_feature">
    <dist:module
        dist:instant="false"
        dist:onDemand="true"
        dist:title="@string/title_my_dynamic_feature">
        <dist:fusing dist:include="true" />
    </dist:module>
    <application>
        <activity android:name=".MyFeatureActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <action android:name=
                  "''<com.yourcompany>''.my_dynamic_feature.MyFeatureActivity" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Complete the feature by loading the activity_my_feature.xml file into the layout editor tool and adding a TextView object displaying text which reads “My Dynamic Feature Module”:


As 3.4 dynamic feature activity ui.png


Figure 1-7

Implementing the launchIntent() Method

With the project structure and user interface designs completed it is time to use the Play Core Library to begin working with the dynamic feature module. The first step is to implement the launchFeature() method, the purpose of which is to use an intent to launch the dynamic feature activity. Attempting to start an activity in a dynamic feature module that has yet to be installed, however, will cause the app to crash. The launchFeature() method, therefore, needs to include some defensive code to ensure that the dynamic feature has been installed. To achieve this, we need to create an instance of the SplitInstallManager class and call the getInstalledModules() method of that object to check whether the my_dynamic_feature module is already installed. If it is installed, the activity contained in the module can be safely launched, otherwise a message needs to be displayed to the user on the status TextView. Within the MainActivity.java file, make the following changes to the class:

.
.
import android.widget.TextView;
import android.view.View;
import android.content.Intent;
.
.
import com.google.android.play.core.splitinstall.SplitInstallManager;
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
.
.
public class MainActivity extends AppCompatActivity {
 
    private TextView statusText;
    private SplitInstallManager manager;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        statusText = findViewById(R.id.status_text);
        manager = SplitInstallManagerFactory.create(this);
    }
 
    public void launchFeature(View view) {
        if (manager.getInstalledModules().contains("my_dynamic_feature")) {
            Intent i = new Intent(
                   "com.ebookfrenzy.my_dynamic_feature.MyFeatureActivity");
            startActivity(i);
        } else {
            statusText.setText("Feature not yet installed");
        }
    }
.
.
}

Uploading the App Bundle for Testing

The project is now ready for the first phase of testing. This involves generating a release app bundle and uploading it to the Google Play Console. The steps to achieve this were outlined in the chapter entitled “Creating, Testing and Uploading an Android App Bundle” but can be summarized as follows:

  1. Log into the Google Play Console and create a new application.
  2. Work through the steps to configure the app in terms of title, descriptions, categorization and image assets (sample image assets can be found in the image_assets folder of the sample code download that accompanies this book) and save the draft settings.
  3. Use the Android Studio Build -> Generate Signed App Bundle / APK... menu to generate a signed release app bundle.
  4. In the Google Play Console, select the App Releases option from the left hand navigation panel, locate the Internal test track section in the resulting screen and click on the Manage button.
  5. On the Internal test screen, click on the Create Release button and click Continue to accept the option to let Google manage key signing tasks. Drag and drop the app bundle file generated above onto the “Android App Bundles and APKs to add” box and click Save.
  6. Complete the final configuration steps including the Content Rating and Pricing & Distribution screens.
  7. Navigate within the console to the App Releases page and click on the Manage button located in the Internal Test track section.
  8. On the Manage testers section add the email addresses of accounts that will be used to test the app on one or more physical devices.
  9. Return to the Internal test screen and click on the Edit Release button. Scroll to the bottom of the page and click on the Review button.
  10. On the confirmation screen, click on the Start Rollout to Internal Test button. The app will be listed as Pending publication. Once it has been approved by Google (a process that can take anywhere from a few minutes to a few hours), the app is ready for testing.

Once the release has been rolled out for testing and been approved, notifications will be sent out to all users on the internal testing track asking them to opt in to the test track for the app. Alternatively, copy the Opt-in URL listed on the Manage testers screen and send it to the test users. Once the users have opted in, a link will be provided which, when opened on a device, will launch the Play Store app and open the download page for the DynamicFeature app as shown in Figure 87-8:


As 3.4 install dynamic feature app.png


Figure 1-8

Click on the button to install the app, then open it and tap the Launch Feature button. The fact that the dynamic feature has yet to be installed should be reflected in the status text.

Implementing the installFeature() Method

The installFeature() method will create a SplitInstallRequest object for the my_dynamic_feature module, then use the SplitInstallManager instance to initiate the installation process. Listeners will also be implemented to display Toast messages indicating the status of the install request. Remaining in the MainActivity.java file, add the installFeature() method as follows:

.
.
import android.widget.Toast;
.
.
import com.google.android.play.core.splitinstall.SplitInstallRequest;
import com.google.android.play.core.tasks.OnFailureListener;
import com.google.android.play.core.tasks.OnSuccessListener;
.
.
    public void installFeature(View view) {
 
        SplitInstallRequest request =
                SplitInstallRequest
                        .newBuilder()
                        .addModule("my_dynamic_feature")
                        .build();
 
        manager.startInstall(request)
                .addOnSuccessListener(new OnSuccessListener<Integer>() {
                    @Override
                    public void onSuccess(Integer sessionId) {
                        Toast.makeText(MainActivity.this, 
                           "Module installation started", 
                             Toast.LENGTH_SHORT).show();
                    }
                })
                .addOnFailureListener(new OnFailureListener() {
                    @Override
                    public void onFailure(Exception exception) {
                        Toast.makeText(MainActivity.this, 
                           "Module installation failed" + exception.toString(), 
                             Toast.LENGTH_SHORT).show();
                    }
                });
    }
.
.
}

Edit the Gradle Scripts -> build.gradle (Module: app) file and increment the versionCode and versionName values so that the new version of the app bundle can be uploaded for testing:</span>

android {
.
.
        versionCode 2
        versionName "2.0"
.
.

Generate a new app bundle containing the changes, then return to the Release Management -> App Releases page for the app in the Google Play Console, click on the Manage button in the Internal test track section followed by the Create Release button as highlighted in Figure 87-9 below:


As 3.4 dynamic feature console new release.png


Figure 1-9

Follow the steps to upload the new app bundle and roll it out for internal testing. On the testing device, open the Google Play Store app and make sure the Install button has changed to an Update button (if it has not, close the App Store app and try again after waiting a few minutes) and update to the new release. Once the update is complete, run the app, tap the Install Feature button and check the Toast messages to ensure the installation started successfully. A download icon should briefly appear in the status bar at the top of the screen as the feature is downloaded. Once the download is complete, tap the Launch Feature button to display the second activity.

If the installation fails, or the app crashes, the cause can usually be identified from the LogCat output. To view this output, connect the device to your development computer, open a terminal or command prompt window and execute the following command:

adb logcat

This adb command will display LogCat output in real time, including any exception or diagnostic errors generated when the app encounters a problem or crashes.

Adding the Update Listener

For more detailed tracking of the installation progress of a dynamic feature module, an instance of SplitInstallStateUpdatedListener can be added to the app. Since it is possible to install multiple feature modules simultaneously, some code needs to be added to keep track of the session IDs assigned to the installation processes. Begin by adding some imports, declaring a variable in the MainActivity.java file to contain the current session ID and modifying the installFeature() method to store the session ID each time an installation is successfully started and to register the listener:

.
.
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;
.
.
import java.util.Locale;
.
.
public class MainActivity extends AppCompatActivity {
 
    private TextView statusText;
    private SplitInstallManager manager;
    private int mySessionId = 0;
.
.
   public void installFeature(View view) {
.
.
        manager.registerListener(listener);
 
        manager.startInstall(request)
                .addOnSuccessListener(new OnSuccessListener<Integer>() {
                    @Override
                    public void onSuccess(Integer sessionId) {
                        mySessionId = sessionId;
                        Toast.makeText(MainActivity.this, 
                           "Module installation started", 
                               Toast.LENGTH_SHORT).show();
                    }
                })
.
.

Next, implement the listener code so that it reads as follows:

SplitInstallStateUpdatedListener listener = 
                        new SplitInstallStateUpdatedListener() {
    @Override
    public void onStateUpdate(SplitInstallSessionState state) {
 
        if (state.sessionId() == mySessionId) {
            switch (state.status()) {
                case SplitInstallSessionStatus.DOWNLOADING:
                    long size = state.totalBytesToDownload();
                    long downloaded = state.bytesDownloaded();
                    statusText.setText(String.format(Locale.getDefault(), 
                         "%d of %d bytes downloaded.", downloaded, size));
                    break;
 
                case SplitInstallSessionStatus.INSTALLING:
 
                    statusText.setText("Installing feature");
                    break;
 
                case SplitInstallSessionStatus.DOWNLOADED:
 
                    statusText.setText("Download Complete");
                    break;
 
                case SplitInstallSessionStatus.INSTALLED:
 
                    statusText.setText("Installed - Feature is ready");
                    break;
 
                case SplitInstallSessionStatus.CANCELED:
 
                    statusText.setText("Installation cancelled");
                    break;
 
                case SplitInstallSessionStatus.PENDING:
 
                    statusText.setText("Installation pending");
                    break;
 
                case SplitInstallSessionStatus.FAILED:
 
                    statusText.setText("Installation Failed. Error code: " + 
						state.errorCode());
            }
        }
    }
};

The listener catches many of the common installation states and updates the status text accordingly, including providing information about the size of the download and a running total of the number of bytes downloaded so far. Before proceeding, add onPause() and onResume() lifecycle methods to ensure that the listener is unregistered when the app is not active:

@Override 
public void onResume() {
    manager.registerListener(listener);
    super.onResume();
}
 
@Override
public void onPause() {
    manager.unregisterListener(listener);
    super.onPause();
}

Test that the listener code works by incrementing the version number information, generating and uploading a new release app bundle and rolling it out for testing. If a dynamic feature module is already installed on a device, attempts to download the module again will be ignored by the Play Core Library classes. To fully test the listener, therefore, the app must be uninstalled from within the Play Store app before installing the updated release. Of course, with the app removed there will be no Update button to let us know that the new release is ready to be installed. To find out which release the Play Store app will install, scroll down the app page to the Read More button, select it and check the Version field in the App Info section as highlighted in Figure 87-10 below:


As 3.4 dynamic feature app store details.png


Figure 1-10

If the previous version is still listed, exit the Play app and wait a few minutes before checking again. Once the new version is listed, complete the installation, open the app and check that the status text updates appropriately when the dynamic feature module is downloaded.

Handling Large Downloads

The next task for this project is to integrate support for large feature modules by adding an additional case to the switch statement in the listener implementation as follows:

.
.
import android.content.IntentSender;
.
.
public class MainActivity extends AppCompatActivity {
.
.
    private static final int REQUEST_CODE = 101;
.
.
SplitInstallStateUpdatedListener listener = 
                        new SplitInstallStateUpdatedListener() {
    @Override
    public void onStateUpdate(SplitInstallSessionState state) {
 
        if (state.sessionId() == mySessionId) {
            switch (state.status()) {
 
                case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION:
 
                    statusText.setText(
                         "Large Feature Module. Requesting Confirmation");
 
                    try {
                        manager.startConfirmationDialogForResult(state, 		
					MainActivity.this, REQUEST_CODE);
                    } catch (IntentSender.SendIntentException ex) {
                        statusText.setText("Confirmation Request Failed.");
                    }
                    break;
.
.

Also, add an onActivityResult() method so that the app receives notification of the user’s decision:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE) {
        if (resultCode == RESULT_OK) {
            statusText.setText("Beginning Installation.");
        } else {
            statusText.setText("User declined installation.");
        }
    }
}

Before these code changes can be tested, the size of the dynamic feature module needs to be increased above the 10MB limit. To achieve this we will add a large file as an asset to the project. Begin by right-clicking on the my_dynamic_feature entry in the Project tool window and selecting the New -> Folder -> Assets Folder menu option. In the configuration dialog, change the Target Source Set menu to main before clicking on the Finish button.

Next, download the following asset file to a temporary location on your development system:

https://www.ebookfrenzy.com/code/LargeAsset.zip

Once downloaded, locate the file in a file system browser, copy it and then paste it into the assets folder in the Android Studio Project tool window, clicking OK in the confirmation dialog:


As 3.4 add new dynamic large asset.png


Figure 1-11

Repeat the usual steps to increment the version number, build the new app bundle, upload it to Google Play and roll out for testing. Delete the previous release from the device, install the new release and attempt to perform the feature download to test that the confirmation dialog appears as shown in Figure 87-12 below:


As 3.4 large feature request.png


Figure 1-12

Using Deferred Installation

Deferred installation causes dynamic feature modules to be downloaded in the background and will be completed at the discretion of the operating system. When a deferred installation is initiated, the listener will be called with a pending status, but it is not otherwise possible to track the progress of the installation aside from checking whether or not the module has been installed.

To try deferred installation, modify the installFeature() method so that it reads as follows:

public void installFeature(View view) {
    manager.deferredInstall(Arrays.asList("my_dynamic_feature"));
}

Note that the deferredInstall() method is passed an array, allowing the installation of multiple modules to be deferred.


Removing a Dynamic Module

The final task in this project is to implement the deleteFeature() method as follows in the MainActivity.java file:

.
.
import java.util.Arrays;
.
.
public void deleteFeature(View view) {
    manager.deferredUninstall(Arrays.asList("my_dynamic_feature"));
}

The removal of the feature will be performed in the background by the operating system when additional storage space is needed. As with the deferredInstall() method, multiple features may be scheduled for removal in a single call.

Summary

This chapter has worked through the creation of an example app designed to demonstrate the use of dynamic feature modules within an Android app. Topics covered included the creation of a dynamic feature module and the use of the classes and methods of the Play Core Library to install and manage a dynamic feature module.