The struggles of upgrading our Instant enabled app to Android App Bundles

Menno Vogel
NOS Digital
Published in
6 min readSep 8, 2020
NOS blueprint

Introduction

In 2017 we decided to use the instant app (now called Google Play Instant) feature as we have described in an earlier post:

Now, three years later, Google has introduced a new way to build an app: Android App Bundles. This new format comes with several benefits, like a smaller app download size and faster build times. But the most important reason for us to upgrade to App Bundles was that Google has decided to deprecate the way we built our instant app. This means we would get stuck with an older Gradle (the Android build tool) version if we didn’t migrate to the new App Bundles format. We still have many users on our instant-app as you can see in the graph below, so we decided to follow Google’s advice and upgrade to support Android App Bundles.

monthly Instant-app starts

Now this wasn’t as quick and easy to do as you might expect after reading Google’s own migration guide. So we would like to share our experiences, now that we have done the migration. We will tell you about our step by step process we used to migrate to App Bundles. We have an app with Google Play Instant, but this blogpost might also be useful if your project does not support this as you might run into the same issues.

Issues we’ll discuss in this blogpost:

  • duplicate resources
  • moving to a single Application class
  • build errors when running unit tests
  • issues with Continuous Integration
  • issues with the instant app and the Android Manifest.

It all started with research

Because a lot of files would be moved with the migration, we couldn’t just start and solve the problems, while we were continuing development on other features. This would result in too many merge conflicts. So, we decided to start with a proof of concept. In this PoC we could experiment with different solutions to make sure that the final migration would be quick and easy.

There are several big changes that we have to work with. There is now only one final package, for both the instant app and the regular app. Because we have one package, we also only have one Application and one (merged) AndroidManifest file. Another change is that all resource references can no longer be overridden . And if you need a resource from another module, you will need to access it through another R import.

We have followed Google’s migration guide.

Resource references

An issue that we ran into was resource references that could not be found. “References to R resources from the feature module to the base module cannot be found”. This means overriding resources also doesn’t work. For most cases the error can be solved by changing the import line so it references to the base module instead of the feature module.

Another option could be to duplicate resources to the feature module, but this is obviously not ideal.

Getting the correct resource ID by using a class that is injected from the base module could also be a solution.

In the end we solved hundreds of different errors, most of them were easy to solve using the methods described above.

One class to rule them all

Another issue is that we couldn’t override the application class in the AndroidManifest.xml anymore, so we were stuck with only one application class. We used to have an InstantAppApplication, a DebugApplication, a UiTestApplication, and an InstalledApplication. We used these different Application classes mainly for building our injection graph. This clearly caused some issues for us.

To solve this we decided to use reflection to build our injection graph from the installed module, while the Application class was in the base module. While using reflection is not ideal, we think this was the best option. Google has also made an example showcasing a Dagger implementation in combination with feature modules.

Working with unit tests

After making all the changes, we couldn’t run our unit tests from the command line. They still worked from Android Studio though. This issues has been reported to Google: https://issuetracker.google.com/issues/123441249.

After reading the discussion we solved this issue with the following workaround. We have added this code to our build.gradle in our project root.

allprojects {
afterEvaluate {
def isTestTask = false
gradle.startParameter.taskNames.each { value ->
if (value.contains("test")) {
isTestTask = true
}
}

if (pluginManager.hasPlugin("com.android.dynamic-feature") && isTestTask) {
def appExt = extensions.findByType(AppExtension)

// Workaround for
// https://issuetracker.google.com/issues/123441249
appExt.applicationVariants.all { dependencies.add("test${name.capitalize()}RuntimeOnly",
files("${rootProject.projectDir}/app/build/intermediates/" +
"app_classes/$name/classes.jar")
)
}
}
}
}

This workaround is not ideal however, as the location of build files can change when updating Gradle. Hopefully it will be fixed soon, so this script can be removed.

Continuous Integration

Running instrumentation tests didn’t work out of the box with Bitrise (our CI tool). It looks like Bitrise supports Android App Bundles (aab) files, but we couldn’t get this to work. So we created apk files with the following script.

#!/bin/bash# Download BuildTool, generate installable apkcurl -O -L https://github.com/google/bundletool/releases/download/0.13.4/bundletool-all.jar
./gradlew :app:bundleDebug
java -jar bundletool-all.jar build-apks --bundle app/build/outputs/bundle/debug/app.aab --ks=keystore/debug.keystore --ks-key-alias=androiddebugkey --ks-pass=pass:android --output temp/debug.apks --overwrite --local-testing --mode=universal
unzip temp/debug.apks -d temp/
# The installeble apk can now be found as temp/universal.apk

This script will download Bundletool and uses it to create a debug apk file. It is important to use the same keystore file for creating this apk as you use when you create the androidTest apk. Or else the following error will be shown:

Failed to get test status, error: Failed to get test status: INVALID(NO_SIGNATURE)

After the script is used to create the installable apk, we would need to create the androidTest apk, which is used to run the instrumentation tests. This can be done with the following simple Gradle command:

:app_installed:assembleDebugAndroidTest

The apk can then be found at */app_installed/build/outputs/apk/androidTest/debug/app_installed-debug-androidTest.apk

Now that we have both the installable apk and the androidTest apk we can run the instrumentation tests on Bitrise.

Android Manifest

At this point we released our installable app. But the instant app still didn’t work.

Why not?

When using App Bundles, it is no longer possible to override values in AndroidManifest.xml files. This is what we did in app_installed/…/AndroidManifest.xml:

<!--Overwrites the instant app filters-->
<activity android:name="nl.nos.app.activity.ArticlePagerActivity"
android:theme="@style/Theme.NOS.DayNight.NoActionBar"
tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<!-- Register 'app_installed' routes -->
<data android:host="nos.nl"/>
<data android:scheme="https"/>
<data android:pathPattern="/.*/artikel/.*"/>
</intent-filter>
</activity>

We used tools:node=”replace” because we use different intent filters in the instant app than in the installed app. But with the instant app tools:node=”replace” doesn’t work anymore. The app always uses the same AndroidManifest file, as it’s the same for the instant app as for the installable app. The only difference is that one or more modules are included or excluded in the app bundle.

We found out that we can do this with an <activity-alias> tag. We also decided that this would be the right time to rewrite how we handle links, so we would have a single point of entry. After doing so, our instant app was ready to be published. Here is an example of how we use the <activity-alias> tag in the AndroidManifest.xml file.

<activity-alias
android:name="nl.nos.app.activity.RoutingActivity"
android:targetActivity="nl.nos.app.activity.RoutingActivity"
android:splitName="${moduleName}"
tools:targetApi="o">

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>

<!-- Register 'app_installed' routes -->
<data android:scheme="https" />
<data android:host="nos.nl" />
<data android:pathPattern="/.*/artikel/.*"/>
</intent-filter>

</activity-alias>

That’s it

We’ve been working with the app like this for a while now and it works like a charm. We hope reading this post has been useful. Please let us know if you have any feedback by leaving a comment down below.

--

--