Patchwork Plaid — A modularization story
How and why we modularized Plaid and what’s to come
This article dives deeper into the modularization portion of Restitching Plaid.
In this post I’ll cover how we refactored Plaid away from a monolithic universal application to a modularized app bundle. These are some of the benefits we achieved:
- more than 60% reduction in install size
- greatly increased code hygiene
- potential for dynamic delivery, shipping code on demand
During all of this we did not make changes to the user experience.
A first glance at Plaid
Plaid is an application with a delightful UI. Its home screen displays a stream of news items from several sources.
News items can be accessed in more detail, leading to separate screens.
The app also contains “search” functionality and an “about” screen. Based on these existing features we selected several for modularization.
The news sources, (Designer News and Dribbble), became their own dynamic feature module. The
search features also were modularized into dynamic features.
Dynamic features allow code to be shipped without directly including it in the base apk. In consecutive steps this enables feature downloads on demand.
What’s in the box — Plaid’s construction
Like most Android apps, Plaid started out as a single monolithic module built as a universal apk. The install size was just under 7 MB. Much of this data however was never actually used at runtime.
From a code point of view Plaid had clear boundary definitions through packages. But as it happens with a lot of codebases these boundaries were sometimes crossed and dependencies snuck in. Modularization forces us to be much stricter with these boundaries, improving the separation.
The biggest chunk of unused data originates in Bypass, a library we use to render markdown in Plaid. It includes native libraries for multiple CPU architectures which all end up in the universal apk taking up around 4MB. App bundles enable delivering only the library needed for the device architecture, reducing the required size to around 1MB.
Many apps use rasterized assets. These are density dependent and commonly account for a huge chunk of an app’s file size. Apps can massively benefit from configuration apks, where each display density is put in a separate apk, allowing for a device tailored installation, also drastically reducing download and size.
Plaid relies heavily on vector drawables to display graphical assets. Since these are density agnostic and save a lot of file size already the data savings here were not too impactful for us.
Stitching everything together
During the modularization task, we initially replaced
./gradlew assemble with
./gradlew bundle. Instead of producing an Android PacKage (apk), Gradle would now produce an Android App Bundle (aab). An Android App Bundle is required for using the dynamic-feature Gradle plugin, which we’ll cover later on.
Android App Bundles
Instead of a single apk, AABs generate a number of smaller configuration apks. These apks can then be tailored to the user’s device, saving data during delivery and on disk. App bundles are also a prerequisite for dynamic feature modules.
Configuration apks are generated by Google Play after the Android App Bundle is uploaded. With app bundles being an open spec and Open Source tooling available, other app stores can implement this delivery mechanism too. In order for the Google Play Store to generate and sign the apks the app also has to be enrolled to App Signing by Google Play.
What did this change of packaging do for us?
Plaid is now more than 60 % smaller on device, which equals about 4 MB of data.
This means that each user has some more space for other apps.
Also download time has improved due to decreased file size.
Not a single line of code had to be touched to achieve this drastic improvement.
The overall approach we chose for modularizing is this:
- Move all code and resources into a core module.
- Identify modularizable features.
- Move related code and resources into feature modules.
The above graph shows the current state of Plaid’s modularization:
shared dependenciesare included in core
- dynamic feature modules depend on
:app module basically is the already existing
com.android.application, which is needed to create our app bundle and keep shipping Plaid to our users. Most code used to run Plaid doesn’t have to be in this module and can be moved elsewhere.
To get started with our refactoring, we moved all code and resources into a
com.android.library module. After further refactoring, our
:core module only contains code and resources which are shared between feature modules. This allows for a much cleaner separation of dependencies.
A forked third party dependency is included in core via the
:bypass module. Additionally, all other gradle dependencies were moved from
:core, using gradle’s
api dependency keyword.
Gradle dependency declaration: api vs implementation
api instead of
implementation dependencies can be shared transparently throughout the app. While using
api makes our dependencies easily maintainable because they are declared in a single file instead of spreading them across multiple
build.gradle files this can slow down builds.
So instead of our initial approach we reverted to
implementation, which requires us to be more explicit about the dependency declaration but tends to make our incremental builds faster.
Gradle figures out where stuff is needed and where to place code in the app bundle. This also can make incremental…github.com
Dynamic feature modules
Above I mentioned the features we identified that can be refactored into
com.android.dynamic-feature modules. These are:
A dynamic feature module is essentially a gradle module which can be downloaded independently from the base application module. It can hold code and resources and include dependencies, just like any other gradle module. While we’re not yet making use of dynamic delivery in Plaid we hope to in the future to further shrink the initial download size.
The great feature shuffle
After moving everything to
:core, we flagged the “about” screen to be the feature with the least inter-dependencies, so we refactored it into a new
:about module. This includes Activities, Views, code which is only used by this one feature. Also resources such as drawables, strings and transitions were moved to the new module.
We repeated these steps for each feature module, sometimes requiring dependencies to be broken up.
In the end,
:core contained mostly shared code and the home feed functionality. Since the home feed is only displayed within the application module, we moved related code and resources back to
A closer look at the feature structure
Compiled code can be structured in packages. Moving code into feature aligned packages is highly recommended before breaking it up into different compilation units. Luckily we didn’t have to restructure since Plaid already was well feature aligned.
As I mentioned, much of the functionality of Plaid is provided through news sources. Each of these consists of remote and local data source, domain and UI layers.
Data sources are displayed in both the home feed and, in detail screens, within the feature module itself. The domain layer was unified in a single package. This had to be broken in two pieces: a part which can be shared throughout the app and another one that is only used within a feature.
Reusable parts were kept inside of the
:core library, everything else went to their respective feature modules. The data layer and most of the domain layer is shared with at least one other module and were kept in core as well.
We also made changes to package names to reflect the new module structure.
Code only relevant only to the
:dribbble feature was moved from
io.plaidapp.dribbble. The same was applied for each feature within their respective new module names.
This means that many imports had to be changed.
Modularizing resources caused some issues as we had to use the fully qualified name to disambiguate the generated
R class. For example, importing a feature local layout’s views results in a call to
R.id.library_image while using a drawable from
:core in the same file resulted in calls to
We mitigated this using Kotlin’s import aliasing feature allowing us to import core’s
R file like this:
import io.plaidapp.core.R as coreR
That allowed to shorten the call site to
This makes reading the code much more concise and resilient than having to go through the full package name every time.
Preparing the resource move
Resources, unlike code, don’t have a package structure. This makes it trickier to align them by feature. But by following some conventions in your code, this is not impossible either.
Within Plaid, files are prefixed to reflect where they are being used. For example, resources which are only used in
:dribbble are prefixed with
Further, files that contain resources for multiple modules, such as styles.xml are structurally grouped by module and each of the attributes prefixed as well.
To give an example: Within a monolithic app,
strings.xml holds most strings used throughout.
In a modularized app, each feature module holds on to its own strings.
It’s easier to break up the file when the strings are grouped by feature before modularizing.
Adhering to a convention like this makes moving the resources to the right place faster and easier. It also helps to avoid compile errors and runtime crashes.
Challenges along the way
To make a major refactoring task like this more manageable it’s important to have good communication within the team. Communicating planned changes and making them step by step helped us to keep merge conflicts and blocking changes to a minimum.
The dependency graph from earlier in this post shows, that dynamic feature modules know about the app module. The app module on the other hand can’t easily access code from dynamic feature modules. But they contain code which has to be executed at some point.
Without the app knowing enough about feature modules to access their code, there is no way to launch activities via their class name in the
Intent(ACTION_VIEW, ActivityName::class.java) way.
There are multiple other ways to launch activities though. We decided to explicitly specify the component name.
To do this we created an
AddressableActivity interface within core.
Using this approach, we created a function that unifies activity launch intent creation:
In its simplest implementation an
AddressableActivity only needs an explicit class name as a String. Throughout Plaid, each
Activity is launched through this mechanism. Some contain intent extras which also have to be passed through to the activity from various components of the app.
You can see how we did this in the whole file here:
Instead of a single
AndroidManifest for the whole app, there are now separate
AndroidManifests for each of the dynamic feature modules.
These manifests mainly contain information relevant to their component instantiation and some information concerning their delivery type, reflected by the
This means activities and services have to be declared inside the feature module that also holds the relevant code for this component.
We encountered an issue with modularizing our styles; we extracted styles only used by one feature out into their relevant module, but often they built upon
:core styles using implicit inheritance.
These styles are used to provide corresponding activities with themes through the module’s
Once we finished moving them, we encountered compile time issues like this:
* What went wrong:
Execution failed for task ‘:app:processDebugResources’.
> Android resource linking failed
error: resource style/Plaid.Translucent.About (aka io.plaidapp:style/Plaid.Translucent.About) not found.
error: failed processing manifest.
The manifest merger tries to merge manifests from all the feature modules into the app’s module. That fails due to the feature module’s
styles.xml files not being available to the app module at this point.
We worked around this by creating an empty declaration for each style within
styles.xml like this:
<! — 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” />
Now the manifest merger picks up the styles during merging, even though the actual implementation of the style is being introduced through the feature module’s styles.
Another way to avoid this is to keep style declarations in the core module. But this only works if all resources referenced are in the core module as well. That’s why we decided to go with the above approach.
Instrumentation test of dynamic features
Along the modularization we found that instrumentation tests currently can’t reside within the dynamic feature module but have to be included within the application module. We’ll expand on this in an upcoming blog post on our testing efforts.
What is yet to come?
Dynamic code loading
We make use of dynamic delivery through app bundles, but don’t yet download these after initial installation through the Play Core Library. This would for example allow us to mark news sources that are not enabled by default (Product Hunt) to only be installed once the user enables this source.
Adding further news sources
Throughout the modularization process, we kept in mind the possibility of adding further news sources. The work to cleanly separate modules and the possibility of delivering them on demand makes this more compelling.
We made a lot of progress to modularize Plaid. But there’s still work to do. Product Hunt is a news source which we haven’t put into a dynamic feature module at this point. Also some of the functionality of already extracted feature modules can be evicted from core and integrated into the respective features directly.
So, why did we decide to modularize Plaid?
Going through this process, Plaid is now a heavily modularized app. All without making changes to the user experience. We did reap several benefits in our day to day development from this effort:
Plaid is now on average more than 60 % smaller on a user’s device.
This makes installation faster and saves on precious network allowance.
A clean debug build without caches now takes 32 instead of 48 seconds.*
All the while increasing from ~50 to over 250 tasks.
This time saving is mainly due to increased parallel builds and compilation avoidance thanks to modularization.
Further, changes in single modules don’t require recompilation of every single module and make consecutive compilation a lot faster.
We have detangled all sorts of dependencies throughout the process, which makes the code a lot cleaner. Also, side effects have become rarer. Each of our feature modules can be worked on separately with few interactions between them. The main benefit here is that we have to resolve a lot less merge conflicts.
We’ve made the app more than 60% smaller, improved on code structure and modularized Plaid into dynamic feature modules, which add potential for on demand delivery.
Throughout the process we always maintained the app in a state that could be shipped to our users. You can switch your app to emit an Android App Bundle today and save install size straight away. Modularization can take some time but is a worthwhile effort (see above benefits), especially with dynamic delivery in mind.
Go check out Plaid’s source code to see the full extent of our changes and happy modularizing!