Traveloka Android Dynamic Feature Module: The Initial Migrated Modules

Yusuf Cahyo Nugroho
Traveloka Engineering Blog
10 min readOct 27, 2021

Authors: Anisha Inas Izdihar (Android Software Engineer) & Yusuf Cahyo Nugroho (Android Software Engineer)

Editor’s Note:

This is the second out of a three articles in a series about the journey of adopting an Android framework that could deliver Traveloka app’s modules to devices across regions/locales dynamically, which had focused previously on the adoption challenges and this time, on the implementation detail, along with compelling performance measurements that corroborate the framework’s merit.

Anisha Inas Izdihar & Yusuf Cahyo Nugroho are Android software engineers working on developing new Flight’s features to help users discover personalized travel destinations as well as on architecture-related projects such as Dynamic Feature Module migration.

In the previous article, we have shared the challenges we found when migrating a module into Dynamic Feature Module (DFM). Two years after completing the pilot project, Traveloka Android codebase now includes 26 dynamic feature modules.

This article will show you how we make our app support DFM. You will also see how implementing DFM significantly decreases our app build time and initial app download size. But, before that, let’s start with a bit of updates from where we left off from the last article.

Traveloka DFM Updates

First Few Migrated Modules

After the pilot project, we attempted to migrate a digital product top-up feature into DFM. We wanted to make the feature to be installed only on users’ devices with locale ID. Sadly, there was a complication that caused us to fail to migrate the module. We will explain more on this later.

Then, with Budget Planner, where users can explore holiday destinations with budget and timeline in mind, we migrated its module into DFM and configured the feature to be downloaded post-installation successfully.

The next module we migrated was the Gift Voucher. This feature allows us to give our loved ones Traveloka vouchers to spend in our app. Unlike Budget Planner, some Gift Voucher features are essential and need to be accessible without downloading the module first, which led to a new problem we hadn’t encountered before. We will explain more on the issue in the Removable Modules Examples and Problems section.

Current DFM Stats in Traveloka Android App

We are still migrating other modules into DFM to date. As of Android app version 3.36, we have migrated 26 out of 35 product modules, with the following delivery options:

  1. Install-time: 15 modules
  2. On-demand: 7 modules
  3. Conditional: 4 modules

The three delivery options will be explained in the Delivery Options section below.

Android DFM Updates

Google finally added their support to shrink resources in dynamic feature modules with the Android Gradle Plugin 4.2 update. The previous article explained how we needed to remove unused resources manually because the resource shrinker was not compatible with DFM. With that update, we can now abandon the tedious work.

Using DFM

What is DFM?

Google’s DFM allows our app to deliver features based on three delivery options (specified below). Since It uses App Bundle technology under the hood, we must use the App Bundle format (.aab) and also separate those features from the base app module to the individual feature modules.

Why Use DFM?

Some features in the Traveloka app, such as International Connectivity, are only available to Indonesian users. Yet, non-Indonesian users still have the same app size since their devices still download the unusable feature code.

We use the DFM that allows the app to deliver modules according to its delivery option to solve this issue. When Indonesian users download our app from Google Play, the download will include the International Connectivity module. Other non-Indonesian users who want to use the feature need to download it separately after the app is installed.

Delivery Options

Traveloka Android app uses the following three delivery options:

  1. Install-time delivery: Modules, such as core features, are delivered along with app installation from the Play Store.
  2. On-demand delivery: Modules are not delivered along with app installation until users decide to use the on-demand features. We can use this option for less important or rarely used features to save users’ data and space.
  3. Conditional delivery: Modules are delivered install-time or on-demand depending on the users’ device configuration, such as hardware features, locale, etc.

Removable Modules

A removable feature module can be installed or uninstalled after downloading the app for the first time. Thus, the app will handle the situation automatically for the users when calling service inside a yet-to-be-installed module.

Module Listener

Before migrating to DFM, we registered services in the Application class’ onCreate method, located in the app module. However, after converting module dependency to support DFM, the app module no longer has access to initialize product module services.

To solve this issue, we created a Module Listener class in each product module. Since the app module does not depend on a product module, the app module must request access to Module Listener classes via Java Reflection.

If a dynamic feature module is not downloaded yet, the Java Reflection will fail because the class is not yet present. Thus, we need to return a fallback Module Listener, which contains default implementations of the module’s services. We must put the fallback class in the base module in the project, which is always pre-installed and non-removable.

After the user downloads a feature module, the app will re-run the service registration. This time, Java Reflection will succeed when retrieving the Module Listener. As a result, the app won’t need to use the fallback module listener again.

Removable Modules Examples and Problems

1. Digital product top-up
We first tried to deliver a digital product top-up feature dynamically. Unfortunately, as we have mentioned before, there was a complication when migrating the module into DFM.

After refactoring the module, the feature could no longer work. We found an external Software Development Kit (SDK) used by our feature that always threw a general error code on runtime. It was hard to find the root cause because the SDK was obfuscated by its owner/maintainer and the error code was too general. We could not ask the SDK owner to fix this issue because we believed they were not familiar with DFM and had other priorities of their own.

We finally found the root cause after debugging the problem for a long time. DFM migration had moved a native library in the SDK to another directory and that caused the SDK unable to read the native library to calculate a checksum. We tried to verify the root cause by modifying the SDK using Recaf. In short, we altered their checksum calculation to return a pre-computed checksum string. After building the app, the top-up feature worked again, which confirmed our theory.
We reported this problem to the SDK owner. However, they couldn’t give us the support we needed. Since it is not an open-source SDK, we couldn’t create a Pull Request to push our fix. After two long months, we decided to not migrate digital product top-up for now.

2. Budget Planner
Next, we migrated the Budget Planner module into DFM and set the delivery option to on-demand delivery. There was a slight difference in the Android Manifest of a module with an on-demand delivery setting compared to install-time, as shown in the previous article.

Figure 1. Android Manifest with Install-time (left) and on-demand (right) delivery option comparison

In the images above, the pre-installed module uses <install-time> tag, while the on-demand module uses <on-demand> tag. The removable tag is set to false in the left image because the app will include the Accommodation module on first installation and users cannot uninstall the feature. You can read Configure conditional delivery to learn more about delivery configuration.

Fallback module listener

When users try to use the Budget Planner, the app will check whether it is already downloaded or not. If not, the app will use the service implementation in the fallback Module Listener.

The default implementation depends on the business requirements. For example, Traveloka app only uses service in the Budget Planner module to navigate from the Homepage to the Budget Planner landing page. Thus, the fallback navigation method will redirect the users to the loading page and download the code in the background. After the download is complete, the app will redirect users to the Budget Planner landing page.

3. Gift Voucher

The Gift Voucher feature is only available for Indonesian users. Thus, we decided to use a conditional delivery option.

Figure 2. Android Manifest with conditional delivery option

The image in Figure 2 specifies the feature to be downloaded at install-time, only when the users have ID country code. Non-ID users are required to download this feature separately after install time.

Fallback module listener

When users buy a voucher from the app or the Traveloka’s website, users’ MyBooking page stores the voucher even when they have not downloaded the Gift Voucher module yet.

Unlike the Budget Planner, we cannot redirect users to the download page when the app wants to use the Gift Voucher service to show the voucher display. To ensure the voucher still appears even when the feature is not downloaded yet, we need to move the required code to the exact location as the fallback Module Listener. Another option is to duplicate the required code to fallback Module Listener. This way, the app can always have access to them.

DFM Limitations

There are limitations when using DFM. According to Google, installing 50 or more feature modules on a single device, might lead to performance issues such as download and install time from conditional or on-demand delivery. Therefore, we should limit removable modules to 10 for the best performance.

DFM Benefits

Build Time Improvement

To know the build time improvement after migrating to DFM, we measured the performance by comparing the build time before and after the module migration. There are two types of build that we measure: full build and incremental build.

Full Build

If we have never built our source code before or after running a clean build, Gradle will compile all source code without caching. Android Studio might also use a full build when cache is full, after developers change local branches, or clean their projects during development, which prompted us to measure a full build performance.

We measured the performance by excluding all feature modules from our build’s settings. To compare, we also recorded the full build time when we made all modules as library modules and included them in the build. We ran Gradle clean build six times to get the average build time as can be seen in Table 1 below.

Table 1. Full build time. Improvement =
(diff / lib) x 100%

By excluding all feature modules in our build, the build time decreased by 36.82%. Most of the time, product engineers only work on one module. Before implementing DFM, product engineers need to include other modules when building a project. However, if product engineers have migrated the other modules to DFM, they can exclude those unused modules to significantly increase their productivity.

Incremental build

Software Engineers often build their projects each time they want to see how the app reflects their code changes. Incremental compilation reduces the wasteful compilation of the source code and instead compiles only the changed module.

We repeated the steps below three times for each module to get the average incremental build time:

  1. Clean and build the project to create a build cache.
  2. Add a temporary XML file containing a simple view to the module and rebuild the project.
  3. Take note of the build time.
  4. Remove that temporary XML file.

We didn’t change the Java/Kotlin file for this experiment because we found that modifying XML files requires a longer build. Thus, we only changed the XML file to get a more direct comparison. We only took nine modules as a sample because of the time-consuming build. After running the incremental build multiple times as described above, we got the following data:

Table 2. Incremental build time. Improvement =
(diff / lib) x 100%

As you can see, There are significant build times reduction across modules (rightmost column). Before migrating to DFM, the app module depended on the product module. When there are changes in the product module, other modules, including the app module that depends on it, need to be rebuilt again.

After migrating the product module to DFM, there are no more dependent modules. Therefore, when there are changes in the feature module, Gradle will only recompile that single module.

App-size Improvement

In Google Play Console, we can see the generated bundles delivered to users’ devices.

Figure 3. Generated bundles (names redacted for confidentiality)

The base module contains non-removable feature modules and library modules that are delivered at install-time when users download the app from Google Play.

In Figure 3, the bundles’ total size is 41.94 MB (tallied manually). However, the initial download of all users doesn’t include all on-demand bundles. The maximum initial download size for Traveloka app is 41.15 MB. Download for non-Indonesian users only consists of the base bundle at 39.7 MB (a difference of ~1.45MB or ~3.5%).

Before migrating to DFM, the base module size was up to 5 MB bigger than after migration size (at ~45 MB) because non-DFM generates more build files. In the previous article, the library module’s R class contains its resources and the resources from all the modules it depends on.

After installation, the application size grows to around three times the initial download size. As a result, users can save up to 15 MB of storage space (and bandwidth). In addition, we plan to implement DFM on more features in the future, which means an even smaller application size on users’ devices over time.

Hopefully, after seeing the numbers above, you can see how investing in migrating to DFM will be very useful in the future. However, we encountered many challenges along the way that we had not mentioned in this series. Please look forward to our next article, where we share our approach to tackling them, including crashes due to constraint layout on specific OS, missing resources after opening web view, conflicting view IDs, and many more advanced DFM problems.

Curious to know what it’s like to develop for Southeast Asia’s lifestyle superapp? Check out available roles in our team on Traveloka’s career page!

--

--