Everything They Don’t Tell You About Instant Apps and Dynamic Features #1 — Problems with your Dagger setup
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:
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:
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
.
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:
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 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.
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, withoutDaggerApplication
.
Therefore we proposed this solution:
AppComponent
would stay in:app
.IccApplication
would move to:base
, and it would provide aCoreComponent
that was home to anything that really needed to be a Singleton.AppComponent
would depend onCoreComponent
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
.
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)
:
Then in our Activities, we can override onInject
and instantiate the AppComponent
, like the Application class was doing before:
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
:
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
andCwcActivity
rather thanIccApplication
. Remember that the remaining dependencies inAppComponent
are stateless, so this doesn’t result in any behaviour changes.
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:
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
- Everything They Don’t Tell You About Instant Apps and Dynamic Features #2 — Android Manifest merging in Dynamic Feature projects (coming soon)
- Patchwork Plaid — A modularization story — Ben Weiss
- Dependency injection in a multi module project — Ben Weiss
- About Dynamic Delivery — Android Docs
- https://github.com/android/plaid