Android at Scale (with Circuit)

Android at Scale (with Circuit)

As Android apps expand into thousands of modules, developers increasingly struggle with lengthy Gradle Build and Sync times, along with a sluggish and unresponsive IDE. It can be a nightmare waiting 20 minutes for a clean build and 5 minutes for a sync after you change branches, only to struggle with an unresponsive editor... So what if we could just make it so that we have fewer modules?

This post demonstrates how to leverage Circuit, a library from Slack, to build a highly scalable Android app by selectively loading only the modules you need to work on. By unloading unnecessary modules, you can significantly reduce Gradle Build and Android Studio Sync times, making feature iteration faster and improving the overall responsiveness of your development environment while you do it.

Fig. 1 — Before unloading modules with Focus (~40 modules)

Fig. 2 — After unloading modules with Focus (12 modules)

(Project graph visualization provided by "ModuleGraph" from iurysouza)

You can have 1,000+ modules and still only build and launch ~20.

But how? The journey begins with Sample Apps. I first encountered the concept of 'Sample Apps' (aka 'Demo Apps') through Ralf Wondratschek's talk Android at Scale. While I grasped the idea, I wished there had been a sample project for better visualization.

Initially, I saw Sample Apps as separate modules launching Activities/Fragments/Views from project modules without needing the hulking giant of :app to be involved. However, after experimenting with Circuit from Slack, my understanding evolved. Now, I view :app itself as a Sample App launcher, capable of launching just 1 feature or ALL features together. (Which is what our release builds are.)

Circuit's design, based on CashApp's "Broadway" Architecture, has two key properties enabling module unloading:

  1. Circuit replaces Intents with data classes called Screens, offering clear toe-holds for unloading internal counterparts.
  2. If you attempt to navigate to an unloaded module, Circuit handles it gracefully, displaying a simple toast notification: 'Route not available.'

Broadway was designed to be "Architecture at Scale", and it feels like Circuit nailed it in the implementation.

Songify Application Structure

:app - The sample app launcher. Gathers all Circuit Presenter.Factory/Ui.Factory from every currently installed module and it builds a "Circuit" out of them. Sets LoginScreen as our starting feature. After login, it will launch our sample app.

:feature:*:public - Just a Screen data class. Other feature modules can use this public Screen data class to launch the feature contained in its :internal counterpart. These only become unloaded if nothing currently loaded points at them.

:feature:*:internal - Contract/Presenter/View/ui. All the guts of the feature. This module points to any :library:*:public or :feature:*:public that it may need. These can be unloaded.

:feature:*:app - The first part is a Hilt module to "provide" a start Screen or any other unsatisfied dependencies. Secondly, it sets what :internal modules we are going to include as part of our app launch. Only 1 app is loaded at a time. The rest become unloaded.

:library:*:public - Interfaces for UseCases and data models that we have no choice but to expose. (Also support libraries or shared views can be here.) These only become unloaded if nothing currently loaded points at them.

:library:*:internal - Implementations for UseCases. If the :public counterpart is still loaded, then these can still be unloaded, but you would have to provide implementations in your Sample App. (see :feature:home:app)

We can use GraphAssert to strictly enforce our module structure and ensure that we keep graph height low like this.

Summary

By treating the main app as a "Sample App Launcher", we can choose how much or how little of the app we want to build. We can build just features D, E, and F and dance from one to the other.

All of this is possible because Circuit is amazing! If I have to work on 1,000 modules for the rest of my life, I desperately want them to be Circuit. It ignites my imagination and makes apps on a level I never dreamed I would be capable of achieving. (Just wait until you see my Dynamic Feature Modules post I am working on!)

Try It Yourself!

If you want to try out the “Focus”ing for yourself, you can clone the project from here (a collaboration with my nephew James):

https://github.com/JamesBuhanan/Songify

Step 1 — Set which feature to launch in :app/build.gradle

Each feature has an “app” module that we can reference. Here is how we would set the “Search” feature to launch. (We could use a Gradle property for this to prevent having to reset when we change branches.)

Step 2 — Run the “Focus” Gradle plugin from Dropbox:

./gradlew :app:focus --no-configure-on-demand --no-configuration-cache

This is what does the actual unloading of modules from the project. It automatically calculates what your new settings.gradle file needs to be so that it only includes the modules specified by your Sample App.

(The Search sample app will have everything unloaded that is not required to run these 2 internal modules. We use api so that they are propagated to :app/build.gradle)

Step 3— Sync the project in Android Studio

All unnecessary modules have now been unloaded! This should build and sync much faster than if you were to load the entire project. And Android Studio will remain snappy and responsive while you are editing code in enormous projects.

In Part 2, I will explore an alternative Circuit architecture that uses Dynamic Feature Modules (DFMs). I know CashApp makes use of them, so this is exciting... Here is a little taste...

Special Thanks

A big thank you to my nephew James Buhanan for rehabilitating my brain and helping me make something important to me, to Jason Pearson for helping review and tweak things, but above all to Zac Sweers for telling me that "this sounds like it would be a good blog post"!