Everything They Don’t Tell You About Instant Apps and Dynamic Features #1 — Problems with your Dagger setup

Jamie Adkins
Pulselive
Published in
9 min readNov 4, 2019

Creating an Android project that is decoupled enough to be able to release a feature as an instant app is a tough ask. It becomes even more daunting when your starting point is a monolithic, single module project that is 4 years old.

At Pulselive, we built the official Cricket World Cup 2019 app. We published the Cricket World Cup Quiz feature as an instant app, but it turned out to be much more difficult than we first thought, and it ended up taking longer than we expected.

In this series we’ll go over how the Cricket World Cup 2019 app went from a 56MB download to a ~15MB download for the full installed app and 4MB for the instant app, and we’ll show every single problem we hit along the way.

Note: I’ll refer to the project interchangeably as ICC or CWC19. The app changes name based on what major cricket tournament is currently occurring.

This the first of many posts on this subject. Here’s an outline about what you can expect to see in the coming weeks:

  • Problems with your Dagger setup
  • Android Manifest merging in Dynamic Feature projects
  • Android Styles and Themes in Dynamic Feature projects
  • Android Gradle Plugin bugs
  • Constraint Layout bugs
  • Resisting the urge to make everything a Dynamic Feature
  • Navigation in multi module projects
  • The 4MB instant app limit
  • Permissions in Instant apps
  • Misc. instant apps tips and tricks

“You said you released an instant app. Can we see it?”

We released the Quiz feature in the Cricket World Cup 2019 app as an instant app. If you are reading this on an Android device, you can try it out by clicking here:

www.cricketworldcup.com/quiz?id=60

(Warning: This quiz is very hard, even for huge cricket fans)

2020 edit: Our instant app has now moved for the T20 World Cup https://www.t20worldcup.com/quiz?id=64

Preparing your project for Dynamic Features

This series does assume that you know a little bit about what dynamic features are, and how that affects your gradle dependency graph (i.e. features can now depend on your application module). If you are not sure about what dynamic features are, you can read about it in the documentation here.

Where We Started

In the start of this post I said our starting point was a single module, monolithic project. That’s not quite true, by January 2019, every feature in the app had been migrated to clean architecture and the project was modularised horizontally, by architectural layer:

January 2019 — Modularised by architectural layer

While this was slightly better than a single module, as most features were pretty clean and decoupled, it wasn’t helping our build times. :app was still a huge module and it was taking forever to build thanks to a pretty large Dagger graph being non-incrementally processed by kapt.

Our initial feature modularisation had a primary goal of reducing build times, and if we could get an instant app out of that, that’d be a huge bonus.

Google’s open source Plaid app was of particular interest to us, it had just gone through a re-architecture to dynamic features modules, and served as very good source for how to achieve the same in our project.

Ben Weiss, one of the collaborators on Plaid, has written two very useful articles about Plaid’s re-architecture, one about migrating to dynamic features and the other about DI in a multi module project. The latter is what we will be focusing on in this post. CWC19 already had a very large and established Dagger setup, and it quickly became clear that it wasn’t going to work in a project with dynamic feature modules. What follows is the steps we took to migrate the project from a huge monolithic Dagger Android setup to something much closer to Plaid’s DI setup.

Step 1 — Creating a base application module

The first thing we realised is that we couldn’t use :app as our base application module for dynamic features. We could have just moved the Quiz feature into a dynamic feature module that depended on :app, and this would have let us do on demand delivery for the Quiz feature pretty easily. However, that wasn’t going to help us deliver an instant app. At this point we were still shipping ICC as an APK, and it was 56MB. There was zero chance of shipping Quiz as a 4MB instant app if it depended on the :app module.

The next step was pretty obvious then, we would create a new :base module that would become our application module, both :quiz and :app would depend on that, and :app would also become a dynamic feature module that was always installed at install time. The plan was to end up with a gradle dependency graph that looked something like this:

Initial plan for dynamic feature module setup. :app and :quiz are dynamic features that depend on a much smaller :base application module.

Note: Google’s samples suggest that your ‘core’ app experience should live in your base module. This is very difficult to do when you are chasing the 4MB instant app limit. As of October 2019, the only feature we have in our base module is the splash screen.

We realised that turning :app into a dynamic feature module would probably cause its own problems, so before we committed to that, we made :base an android library module, and started moving stuff that we knew that would have to live in :base.

First step towards migrating to dynamic features, creating a :base module that will eventually become the application module.

At the very minimum, the application class would have to be moved to :base. Both :app and :quiz were going to need it, so it made sense to move that to base first. This is where we encountered our first big hurdle.

Step 2— Moving your application class to :base

At this point our custom application class looked something like this:

IccApplication class before we moved it to :base

The problem isn’t obvious at first glance. Both Timber and Fabric will be useful in dynamic feature modules anyway, so those dependencies can go in to the :base module with no issue. The problem lies with the fact that IccApplication is a DaggerApplication and it is the one creating our AppComponent. ICC at this point relied heavily on Dagger Android, and Dagger Android encourages you to set up an AppComponent that knows about pretty much everything.

AppComponent before we migrated to dynamic features

AppComponent included ~90 modules of its own, and even more with all the features using @ContributesAndroidInjector to add to SubComponents to this Dagger graph.

This meant that if we were to move the Application class to :base, we’d also have to move AppComponent, which would mean we have to move pretty much everything into :base, completely defeating the point of creating a small :base module in the first place!

Adopting Plaid’s DI setup would be ideal here. :base should provide a much smaller CoreComponent instead of the mammoth AppComponent, and each feature should have it’s own Component. Unfortunately, it wasn’t that straight forward in our project. Everything in :app is setup with Dagger Android, so features rely on AppComponent to be able add their Subcomponents to the graph using @ContributesAndroidInjector. We were 3 months out from the World Cup starting, migrating every feature away from AppComponent to use their own Component (like in Plaid) was not feasible.

Example Feature Component setup in Plaid. Each feature has its own Component that depends on CoreComponent. Source: https://medium.com/androiddevelopers/dependency-injection-in-a-multi-module-project-1a09511c14b7

After a few days of internal discussion, we came to a few conclusions:

  • We shouldn’t try to redo to Dagger graph for the entire app, any solution that changed as little as possible in the :app module would be a good solution.
  • There are very few dependencies in our Dagger graph that actually need to be Singletons. Mostly everything was stateless.
  • Extending DaggerApplication was unnecessary, we weren’t injecting anything into the Application class, and if we needed to do that, we could do that the normal Dagger way, without DaggerApplication.

Therefore we proposed this solution:

  • AppComponent would stay in :app.
  • IccApplication would move to :base, and it would provide a CoreComponent that was home to anything that really needed to be a Singleton.
  • AppComponent would depend on CoreComponent for those Singleton dependencies.
  • Dagger Android would no longer be present at the Application level, and instead AppComponent would be built by the Activities that needed it.

In theory, we would have to change very little in the :app module. Features that used Dagger Android could continue to add their Subcomponents to the graph, and since most things were stateless, it didn’t matter that AppComponent was now scoped to the Activities rather than the Application.

In practice, this worked very well! The final set of changes (broken up into a few different PRs) ended up touching a lot of files, but most of those were single line changes that removed the @Singleton annotation from dependencies that didn’t need them. Much fewer changes than having to change the Dagger setup in the entire project!

Removing DaggerApplication and DaggerAppCompatActivity

Dagger Android would no longer be present at the Application level, and instead AppComponent would be built by the Activities that needed it.

There is one key change we had to make here to achieve this.

In a normal Dagger Android setup, the Dagger graph is initialised by your Application class that extends DaggerApplication. Your Activities then extend DaggerAppCompatActivity, which calls AndroidInjection.inject(this) to inject itself. Under the hood, this call looks for an AndroidInjector from the Application class. DaggerApplication provides this by implementing HasAndroidInjector. However, since our Application class no longer extends DaggerApplication, DaggerAppCompatActivity will be unable to find an AndroidInjector.

DaggerAppCompatActivity as of time of writing. We can no longer use this because we no longer have a DaggerApplication. Source: https://github.com/google/dagger/blob/8f01526/java/dagger/android/support/DaggerAppCompatActivity.java

To solve this, we came up with our own abstract Activity class, which looks very similar to DaggerAppCompatActivity, but importantly it doesn’t call AndroidInjection.inject(this):

This replaces DaggerAppCompatActivity, since our Application class no longer provides an AndroidInjector

Then in our Activities, we can override onInject and instantiate the AppComponent, like the Application class was doing before:

MainActivity instantiates AppComponent now, not the Application class

Here’s what the other key classes looked like after these changes, notice that they are very similar to the Plaid setup:

IccApplication now lives in :base and (lazily) builds CoreComponent:

We also included a helpful field for grabbing the CoreComponent that will used when other Components are initialised.

CoreComponent also lives in :base and knows very little compared to AppComponent:

AppComponent is still in :app and a few things have changed:

  • It is scoped with @PerActivity rather than @Singleton
  • It depends on CoreComponent
  • It injects into MainActivity and CwcActivity rather than IccApplication. Remember that the remaining dependencies in AppComponent are stateless, so this doesn’t result in any behaviour changes.
AppComponent now injects in to Activities instead

Conclusion

We’ve now successfully decoupled our application class from that big nasty AppComponent, and we can continue working on our instant app without disturbing the current implementation of :app too much. In addition to this, we can also slowly start refactoring features in :app away from being dependent on AppComponent.

What we’ve achieved in this post may not be recommended Dagger practice, but it is a neat solution to a problem that was blocking us migrating to dynamic feature modules. AppComponent is still problematic, it still knows far too much about every feature in :app, but now we can develop new features that don’t need to use AppComponent, and we can migrate existing features away from AppComponent in small chunks. We don’t have to do everything at once.

Next Steps

Back to our instant app journey, here’s how our gradle module graph looked at this point:

Same diagram as before, but now :base contains out CoreComponent and our Application class. We are ready to make :base our application module.

Now that we have a :base module, we’re ready to make that our application module, and turn :app into a dynamic feature module. This will bring on its own set of problems, and we’ll talk about them next time.

Further Reading

--

--