Patchwork Plaid — A modularization story

Illustrated by Virginia Poltrack

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

Navigating 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 about and 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.

Code structure

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.

Native libraries

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.

Drawable resources

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.

Benefits

What did this change of packaging do for us?

Plaid is now 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.

Approaching modularization

The overall approach we chose for modularizing is this:

  1. Move all code and resources into a core module.
  2. Identify modularizable features.
  3. Move related code and resources into feature modules.
green: dynamic features | dark grey: application module | light grey: libraries

The above graph shows the current state of Plaid’s modularization:

  • :bypass and external shared dependencies are included in core
  • :app depends on :core
  • dynamic feature modules depend on :app

Application module

The :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.

Plaid’s core module

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.

External dependencies

A forked third party dependency is included in core via the :bypass module. Additionally, all other gradle dependencies were moved from :app to :core, using gradle’s api dependency keyword.

Gradle dependency declaration: api vs implementation

By utilizing api instead of implementation dependencies can be shared transitively throughout the app. This decreases file size of each feature module, since the dependency only has to be included in a single module, in our case :core. Also it makes our dependencies more maintainable, since they are declared in a single file instead of spreading them across multiple build.gradle files.

Dynamic feature modules

Above I mentioned the features we identified that can be refactored into com.android.dynamic-feature modules. These are:

:about
:designernews
:dribbble
:search

Introducing com.android.dynamic-feature

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 :app.

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.

feature and core modules with their respective architectural layers

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.

Package changes

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 to 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

io.plaidapp.core.R.drawable.avatar_placeholder

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

coreR.drawable.avatar_placeholder

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 dribbble_.

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.

Good intentions

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:

Styling issues

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 dist: tag.
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.

Parts of Plaid’s style hierarchy

These styles are used to provide corresponding activities with themes through the module’s AndroidManifest.

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
~/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.

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 :core’s 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.

Finish modularization

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:

Install size

Plaid is now on average more than 60 % smaller on a user’s device.
This makes installation faster and saves on precious network allowance.

Compile time

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.

*For reference, these are the commits I built for before and after timing.

Maintainability

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.

In conclusion

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!