Everything They Don’t Tell You About Instant Apps and Dynamic Features #2 — Android Manifest Merging

Jamie Adkins
Pulselive
Published in
8 min readApr 2, 2020

Note: Just before I was about to publish this, Google released an update for bundletool, which fixes the very issue that this blog post talks in length about. Still, this series of posts was designed to tell the story of the Cricket World Cup instant app, so I’m going to leave this post as it was. I’ve included some discussion at the end of this post about the fix in bundletool. Enjoy!

This is part 2 in a series detailing how we transformed the Cricket World Cup 2019 app from a 56MB APK to a ~15MB app bundle with a 4MB instant app, aiming to show every single problem we hit along the way.

In part 1, we started with a huge monolithic :app module, and created a :base module that is now home to our Application class. We are now ready to make :base our Application module, and turn :app into a dynamic feature module.

This is where we were at the end of Part 1. We have a :base module that is now ready to become our application module.

There is nothing out of the ordinary for how we configured these, you can follow the documentation on how to setup those modules. For :base, you can use the base module guide. Since you want :app to be a dynamic feature that is always installed when the user installs your app from the Play Store, you should also use the at-install guide.

Your Gradle dependency diagram will now look like this:

:app is now a (very large) dynamic feature module

But your project probably won’t build, since the Android manifest merge will almost certainly fail.

Android Manifest Merging in Dynamic Feature Projects

The error you see will look something like this:

* What went wrong:Execution failed for task ‘:app:processDebugResources’.
> Android resource linking failed
~/plaid/app/build/intermediates/merged_manifests/debug/AndroidManifest.xml:177: AAPT:
error: resource style/Plaid.Translucent.About (aka io.plaidapp:style/Plaid.Translucent.About) not found.
error: failed processing manifest.

Note: I’m gonna use a lot of examples from the Plaid in this blog post. It’s a great sample app. Ben Weiss discusses how Plaid deals with this problem in this post, and we’ll be building off of that solution in this post.

When the Android Gradle Plugin builds your dynamic feature project, it will merge the Android Manifest from your dynamic feature module into the base module’s Android Manifest. Even though :app depends on :base from a Gradle point of view, the :base module will grab the Manifest from :app and merge it with its own, like it would with a library module.

This means that your merged Android Manifest will look something like this:

Pseudo merged manifest taken from Plaid: https://github.com/android/plaid

So the Manifest from dynamic feature :about has been merged into the base module, and the Activity declared in :about says it wants to use a theme.

android:theme="@style/Plaid.Translucent.About"

The important detail here is that only the Manifest has been merged down to the Application module, the style resource Plaid.Translucent.About still only exists in :about. So the manifest gets merged, Android Gradle Plugin then tries to evaluate the Manifest and it can’t find the Plaid.Translucent.About style resource. How can we deal with this?

You could just move all your styles to the :base module. This unfortunately has a knock on effect if you use styles/themes heavily. Do you reference some color resources in your style definition? They need to be moved to :base. Fonts? Those .ttf files need to come into :base. Got some drawables that you reference in your styles? Into :base they go.

Remember, in part 1 that we said we want to keep our :base module as small as possible. It’s going to become part of our instant app, so anything in :base will contribute to the 4MB instant app limit. You really want to avoid having anything in there that doesn’t need to be there.

The ICC/Cricket World Cup app uses styles/themes extensively. Since different tournaments have different branding, it’s not uncommon for us to use different fonts per tournament, and our apps contain lots of images/drawables related to branding. Our instant app is only going to need the Cricket World Cup theme, but if we include all our styles in :base, then the instant app will include fonts and drawables from tournaments that it doesn’t use. That’s going to make our life difficult when we try to get the instant app under 4MB.

How does Plaid solve it?

In this post about Plaid, Ben Weiss suggests the following solution. Since the Android Manifest only needs to know about the id of the style resource, not the implementation, you can create stub styles in :base to make the Manifest happy, and then override them in the dynamic feature module.

<!-— Placeholders. Implementations in feature modules. -->
<style name=”Plaid.Translucent.About” />
<style name=”Plaid.Translucent.DesignerNewsStory” />
<style name=”Plaid.Translucent.DesignerNewsLogin” />
<style name=”Plaid.Translucent.PostDesignerNewsStory” />
<style name=”Plaid.Translucent.Dribbble” />
<style name=”Plaid.Translucent.Dribbble.Shot” />
<style name=”Plaid.Translucent.Search” />

These stub styles in :base satisfy the Manifest merge, and then you can provide the real implementations in the Dynamic Feature module:

Declaring a real implementation of the style in the dynamic feature module

Why didn’t this solution work for us?

Plaid’s solution is really useful, and keeps you from having to put all your styles in :base. However, there are 2 situations where this doesn’t work:

  • You deploy as an app bundle to a device below SDK 21
  • You deploy as a universal APK to any device

There is a quirk in the Android Gradle Plugin where styles are overridden differently in single APK installs vs split APK installs. Since devices below SDK 21 don’t support split APK installs, bundletool (the tool that builds APKs for a device when given an Android App Bundle) will build a single, universal APK for that device. When you run the app, you’ll get a RuntimeException that says your theme doesn’t extend Theme.AppCompat. This is because it’s using the empty stub style in :base, and not the style you declared in your dynamic feature module.

The Cricket World Cup app is min SDK 19. We have a large user base in India, which means we can’t bump to 21 just yet. If we were to use stub styles, then users on devices running < SDK 21 would see a crash as soon as they opened the app.

It also causes problems when giving builds to QA. For convenience, we would normally just give QA a universal APK to test, but they would see the same issue with this setup.

If your project is min SDK 21+, then you may be able to get away with using stub styles by changing your QA workflow. Instead of giving them universal APK builds, you can use Google Play’s new internal app sharing to give them split APK installs that are built from app bundles.

If you are like us and still have a min SDK < 21, then you unfortunately will have to move all your styles to :base. For us, this meant we had to move an additional 300KB set of .ttf font files. Despite the fact that this font is only used in our installed app, it now was included in our instant app download, and it pushed us over the 4MB limit.

Our solution for CWC

We did manage to find a way to remove those font files from :base, but it involved sacrificing the experience a little bit for users on SDK < 21. Instead of using a completely empty stub style like Plaid does, you can put most of your theme in :base, and let your dynamic feature overwrite that theme and include any large resources, such as fonts.

Here’s how this looks:

styles.xml in :base module

In our :base module, we have BaseIccTheme which contains all of our theme except the fonts, and then we create an empty IccTheme that inherits from this theme.

Then in our dynamic feature module, we overwrite IccTheme and add the fonts:

styles.xml in :app dynamic feature module

What does this achieve? Well for all users on SDK 21 and above, they will receive a split APK install, and the theme in the dynamic feature module will be used. They will get everything from BaseIccTheme, but they’ll also get the font that is applied in IccTheme. For users on SDK < 21, they will receive a single APK install, and will receive the wrong theme, they will receive the empty IccTheme that is in the :base module. However, since we’ve moved most of the theme to :base, they will still have all the correct colours and other theme attributes, but they will have the incorrect font. This was a compromise we were willing to make in order to release an instant app. It is not ideal, but we decided that a font issue for <3% users was worth it for an instant app for ~94% of users.

Like in part 1, the solution we ended up using is absolutely not a best practice, but it is a solution that allowed us to get an instant app out of the door. I hope that this helps you make an informed decision about how to deal with the problem in your project. Note: Now that this Android Manifest merging issue has been fixed in bundletool 0.13.4, my suggestion now would be to use Plaid’s solution, see footnote.

In the next post we’ll look some more bugs we encountered in the Android tooling. See you then!

Post Index:

  1. Problems with your Dagger setup
  2. Android Manifest merging in Dynamic Feature projects (this post)
  3. Android tooling bugs (coming soon)
  4. The 4MB download size limit (coming soon)
  5. Permissions in Instant apps (coming soon)
  6. Resisting the urge to make everything a Dynamic Feature (coming soon)

April 2020 — Bundletool fix

After we tried Plaid’s solution to this problem, and encountering the issue of the incorrect style being used on universal APK installs, I raised an issue on Github with bundletool. With incredible timing 1 year later, now that I am about to publish this blog post that discusses the problem in great detail, Google have just released a fix for this issue!

If you are using bundletool 0.13.4 or above, bundletool will merge your manifests slightly differently, using a new FusingAndroidManifestMerger, and as a result, the Android Manifest in universal APKs will now point to the correct style resource; the one in your dynamic feature module.

To use this fix in your project, you need to tell AGP to use the new version of bundletool (until an update to AGP is released that uses bundletool 0.13.4 by default). You can do this by adding bundletool as a dependency in your top level build.gradle:

buildscript {
repositories {
google()
jcenter()
...
}
dependencies {
classpath "com.android.tools.build:bundletool:0.13.4"
...
}
...
}

As described in this comment on Github.

You will also need to wait for Google Play to be updated to use the new version of bundletool. Remember that that when a user installs your app from the Play Store, Google Play uses bundletool to build APKs for that user’s device. In order for this fix to work, those APKs need to be built using bundletool 0.13.4. Google Play will be updated this month to use bundletool 0.13.4.

Once you are using bundletool 0.13.4, you can use Plaid’s stub styles solution in your project, and both universal APK and split APKs installs will behave the same, they will both take the style from your dynamic feature module.

--

--