Android Guild Week: Modularizing our App
By: Todd Santaniello
Tags: Mobile, Android

The Android team at Strava tries to maintain a healthy level of refactoring in conjunction with feature development throughout the year. However, earlier this month, all Android engineers had the opportunity to focus solely on platform health for an entire week (see our post about Guild Weeks for the history). After reviewing potential areas on which to focus, we decided to spend the week dividing our codebase into functional modules. This post reviews our reasoning, approach and outcomes.

Why create more modules?

The Android codebase is approximately 6.5 years old and started, as most projects do, with a single module (now named handset ). Over the years we added a few new modules, but the majority of our code was still concentrated in handset . With improvements to the Android Gradle Plugin (AGP) such as build caching by module and parallel compilation, we set out to modularize our codebase with the following goals:

make the project more readable and less tightly coupled

increase build speed

enable more efficient development iterations (explained later)

Prerequisites

Leading up to Guild Week, we agreed it was important to establish some benchmarks and build tools to track our progress. To address this, we added 2 items to our gradle configuration:

time-based tracking of build tasks — we used Netflix’s Nebula plugin to achieve this simple code distribution counts — we wrote a basic gradle task that runs cloc locally to give a snapshot of distribution across modules

Defining the work

We follow a Scrum process at Strava, so we wrote stories to extract different pieces of our app into new modules. Some examples include:

Create a Challenges module — the Challenges feature is fairly self-contained and seemingly easy to extract

Create a Test Utilities module — we had some test code duplicated across modules and it made sense to move it to a common location that was reusable by all

Create an Authorization module — if we extract the login flow to a separate module, engineers can assemble mini-apps during feature development that allow focused iteration. For example, an engineer building new views for our Clubs module could deploy a “Clubs” app that consists of the authorization, clubs and core modules. This should build and deploy in a fraction of the total handset build.

With our benchmarks established and a week ahead of us, we divvied up the stories and charged forward.

One step at a time

We all started by creating new, empty modules and then proceeded to extract code out of handset into the new modules. This is also when we really started to shine a light on years of accumulated complexities. Two examples highlight the challenges we faced:

Authorization

As mentioned earlier, we knew creating an authorization module would be a big win. The path to extract that code unfolded with the following steps:

getAthlete() — In our codebase, the authorization process revolves around the Athlete class. In the past year, we had already moved a lot of Athlete code from our handset module to an athlete module, but had not yet extracted the fundamental getAthlete() method that queries our API, which is a key step in the authorization flow.

— In our codebase, the authorization process revolves around the Athlete class. In the past year, we had already moved a lot of Athlete code from our module to an module, but had not yet extracted the fundamental method that queries our API, which is a key step in the authorization flow. Push notification settings coupling — When we tried to extract getAthlete() , we saw that the completion callback method syncs push notification settings to our API. Unfortunately, our notification settings code resided in handset and was tightly coupled with several classes there. So we set out to create a notifications module and extract that.

, we saw that the completion callback method syncs push notification settings to our API. Unfortunately, our notification settings code resided in and was tightly coupled with several classes there. So we set out to create a module and extract that. Pencils down — the week ended while we were in the midst of extracting the notification code to a new module. As of this writing, we are finally ready to land those changes.

While we slowly moved the authorization code following the process above, we also built an interim solution to achieve mini-apps:

We added an option to our development menu (not shipped in production builds) that exports preferences and authorization information.

We introduced a new launcher module that consumes the exported preferences and runs a given starting Activity.

module that consumes the exported preferences and runs a given starting Activity. This allows engineers to deploy a local development app with a subset of the desired modules and authorized access to our test servers.

View complexities

Another issue we encountered during the week was untangling legacy Activity and Fragment implementations. Long ago, we added StravaBaseActivity and StravaBaseFragment classes. They started as simple ways to provide things like lifecycle method performance tracking and basic injection of commonly used interfaces. Over time, these classes grew well beyond their scope and violated the OOP principle of Composition over Inheritance. We have been gradually migrating our view implementations to an MVVM pattern, but during Guild Week we discovered many of the legacy Activities and Fragments were effectively trapped in the handset module due to the extensive dependencies on Base classes. To address this, we migrated what Views we could and made plans to provide much lighter Base classes in a common-ui module which will ease the transition out of handset.

Moving forward

Aforementioned complexities aside, we continued our work toward a more modular codebase and made solid progress throughout the week. The team banded together, reviewing Pull Requests and socializing our process to determine how, when and why we introduce a new module. We also ensured a README file accompanied every module that describes its function and assigns ownership to engineers with expertise.

In addition, we improved our code on several other fronts:

Since we recently increased our minSdkVersion , we were able to move our images to webp and reduce our APK footprint.

, we were able to move our images to and reduce our APK footprint. We deleted a lot of unused build configurations and code from app flavors we had deprecated, simplifying our overall gradle structure.

We consolidated all of our string content to a single module. This drastically reduced the complexity in our translation process and simplified the steps necessary to create a new module.

We successfully created a test-utils Java library module. This consolidated all of our shared test classes to a single module that we can now include with a simple testImplementation project(':test-utils') line in any gradle file.

On Monday, we re-ran our tracking tools to measure the efforts and assess our week.

Drawing Conclusions

We arrived at the following conclusions when the week was over:

Modules and code distribution

We introduced 8 new modules and reduced the percentage of the codebase in handset from 77% to 74%. 3% does not seem like a large amount, but in one week we established momentum behind modularization and arrived at the following general process: move things, see what breaks, untangle, repeat.

Subset of code distribution metrics by module before & after Guild Week

Build Speed

Continuous Integration (CI) build times actually increased by 23%. At first this was alarming, as one of the main reasons we began this process was to decrease time spent waiting for our code to build. However, we dug into the numbers further and discovered that due to the changes in the codebase throughout the week, our CI image was spending a lot of time downloading newly-added dependencies. To address this, we increased the frequency at which we rebuild our CI image and continue to monitor build speed closely. We are also investigating a process to automatically rebuild the image every time new dependencies are introduced, eliminating noise from the speed measurements.

Elapsed build times (pink line) during Guild Week

In addition, our interim solution for mini-apps yielded great results regarding build speed. While not tracked in our CI environment, tests on development machines showed a 4x reduction in build time when deploying a mini-app vs deploying the complete handset APK. This will dramatically speed up the development iterations engineers encounter on a daily basis.

Overall, the team had a great time working together towards a cleaner codebase. We’re actively tracking our progress and are eager to see how much we can achieve before our next Guild Week.