4 Comments

Supporting Old Android Versions with Multidex

My current Android project needs to be backwards-compatible with every Android version back to Android 4.4 (KitKat). Meeting this requirement can be difficult and frustrating for a number of reasons, but with KitKat installed on ~10% of all Android phones worldwide (at the time of this writing), it’s still worth supporting.

One of the issues you’ll run into with any Android app—whether or not you’re supporting older OS versions—is the need to multidex your app. With that being said, there are still a few things to watch out for if you’re going to be multidexing an app that runs against older OS versions.

What Is Multidexing?

When an Android application is built, it generates an APK file that contains the compiled source code. All of the classes, methods, assets, etc. that are a part of an Android application are included in this APK. It’s possible to analyze an APK using the tools provided in Android Studio under the Build > Analyze APK… menu option. Doing this helps us acquire a better understanding of what’s happening when our application is built.

Typically, when an application is still relatively small, the generated APK will contain a single DEX file that includes all of the referenced classes and methods from your app. DEX files have a hard limit for how many references they can contain, so after your app reaches a certain size, it will fail to build.

This is where multidexing comes into play. With a few configuration changes, your build process will generate an APK that has multiple DEX files for all of the class/method references in your application.

An Android APK that has been configured for multidexing.

Multidex Support For Older Android Versions

As the documentation above mentions, if your application needs to support Android versions older than API 21 (Lollipop), you will need to use the MultiDex Support Library. Essentially, this involves adding the com.android.support:multidex library to your build.gradle file and overriding the attachBaseContext method in your Application class, like so:


public class MyApplication extends Application {
  @Override
  protected void attachBaseContext(Context base) {
     super.attachBaseContext(base);
     MultiDex.install(this);
  }
}

Having clear documentation about setting this up is incredible, but why is it necessary? Prior to API 21, the Dalvik VM-the virtual machine that older Android versions used to run on-would only work with a single DEX file. A major limitation of the Dalvik VM is that it can only reference the main DEX file in an APK, even if your application has multiple. Calling MultiDex.install(this) appends the additional class/method references from the other DEX files in the APK at run time to support this behavior.

This works well for an application APK, but I ran into issues when the APK generated for our Android instrumented tests needed to be multidexed as well.

The Problem

Android instrumented tests generate their own androidTest APK in addition to the main application APK. After a certain point in time, this androidTest APK will reach the method limit for DEX files and will need to be multidexed. Fortunately, since the application is already configured for this, it will be multidexed without any additional effort.

However, there are a few issues with this approach. This behavior shouldn’t affect Android versions greater than or equal to Lollipop since they support multiple DEX files, but older API versions are likely not going to be able to find your tests.

An Android Test APK that was multidexed with most of the references stored in the second DEX file.

As you can see from the screenshot above, there are two DEX files in this androidTest APK with most of the class/method references contained in the classes2.dex file. classes2.dex is unfortunately not the main DEX file, which means older API versions won’t be able to find those class/method references.

“Hold on, shouldn’t the call to MultiDex.install(this) in the attachBaseContext method take care of this for us?

I focused on this question for quite some time, but ultimately, the issue occurs prior to the application initializing, so we can’t rely on appending the additional DEX files at run time. These Android instrumented tests take the class/method references from your androidTest APK and pass them along to the AndroidJUnitRunner, and since older Android versions aren’t aware of the additional DEX files, they don’t know the majority (or all) of the test methods are in the second DEX file. This causes the AndroidJUnitRunner to report that there were no tests found.

The Solution

Since we can’t append the class/method references from the additional DEX files to the test runner in time, what other options do we have?

Delete tests to prevent multidexing – 🙅🚫

We’re certainly not going to delete tests to prevent the androidTest APK from being multidexed. It manages to solve the problem in the short term, but this means that we can’t add new tests unless we remove old ones. There might be a small opportunity to refactor your test suite to remove redundant tests or consolidate helper methods, but you’ll end up running into this problem again in the future.

Drop support for older Android versions – 🤔❓

Dropping support for these older API versions might be a possibility with Android KitKat (API 19) only making up ~10% of the Android operating system distribution worldwide. Before doing so, I would recommend that you gather some analytics on the Android operating system distribution of your application’s users. There might be ~10% of all Android devices using KitKat globally, but there’s a chance that your application only has a handful of users still on that operating system. In this scenario, dropping support for older Android operating systems might be worthwhile.

Keep required references in main DEX file – 👍✅

Dropping support for KitKat wasn’t an option for our project at this time, so we had to come up with another solution. Since the androidTest APK didn’t have the class/method references we needed in the main DEX file, we needed to figure out a way to make it so. That’s when I stumbled upon ProGuard.

With ProGuard, we can create a proguard-multidex-rules.pro file:


// Need to make sure these classes are available in the 
// main DEX file for API 19

-keep class android.support.test.internal** { *; }
-keep class org.junit.** { *; }
-keep public class com.company.application.acceptance.** { *; }
-keep public class com.company.application.integration.** { *; }

Then, in our build.gradle file we can make sure that we adhere to these ProGuard rules during debug builds by adding this bit of configuration:


android {
  //Other config options

  buildTypes {
    debug {
      // Other debug settings
      multiDexKeepProguard file('proguard-multidex-rules.pro')
    }
    release {
      // Other release settings
    }
  }
}

After cleaning the project and rebuilding the androidTest APK, we should be able to analyze the new APK and see that the class/method references are now included in the main DEX file.

An Android Test APK that was multidexed with ProGuard rules applied.

Now the AndroidJUnitRunner will be able to discover our test classes and start running the Android instrumented tests on older API versions.

I hope that you found this post helpful. How do you support backward-compatibility for older Android versions?