Android Guild Week 2: More Modules
By: Todd Santaniello
Tags: Android, Mobile

In our last blog post on Guild Week 1, the Android Team outlined our efforts dividing the codebase into modules to achieve a greater separation of concerns and also leverage Gradle build speed optimizations. As we approached Guild Week 2, we decided to continue with that effort and dig into some of the nitty-gritty pieces of our top-level handset module. The initial idea for the week was to focus on extracting BaseActivity and BaseFragment classes from the handset module into a common module. This would allow us to move subclasses (e.g. ChallengesActivity) out to functional modules. However, early in the week we took a step back, whiteboarded some ideas and changed our whole modularization approach. Before I get into that, though, it will be helpful to start at the beginning of our journey with Android modularization…

The Strava Android app started, as most apps do, with a single module that contained all of our code. This was in 2011, before we moved to Gradle or shared maven repositories for 3rd party libraries. Over time, we added a few modules to isolate 3rd party library code, but for the most part our app structure looked like Diagram 1 below. The main module depended on several library modules and that was about it.

Diagram 1

When Android Wear was introduced, we started to divide our codebase into modules for reuse. As you can see in Diagram 2, we introduced a wear module that contained our Wear application and we added a common module downstream so both the phone app and the wear app could share visibility to common code. In addition, we started to add 3rd party dependencies through maven and introduced a few Java-only modules downstream that contained easily-testable domain objects. We’ll highlight the significance of this downstream extraction of code later on, but note that modules do not have visibility upstream in this design.

Diagram 2

This brings us to Guild Week 1, when we rallied around the theme of modularization and began moving as much code as we could into functional modules. We introduced 8 new modules that contained code for features such as Challenges, Clubs and Segment Explore. As seen in Diagram 3, the majority of our code (74%) still resided in the handset module.

Diagram 3

Any classes we wanted to pull downstream to a feature module needed to be completely untangled from upstream dependencies. Let’s look at ChallengesActivity as an example:

/**

* Activity that displays Challenges in a list.

*/ public class ChallengesActivity extends StravaToolbarActivity { private static final String TAG = ChallengesActivity.class.getCanonicalName(); @Inject HomeNavBarHelper mHomeNavBarHelper;

@Inject ChallengeGateway mChallengeGateway;

@Inject RxUtils mRxUtils;

Of those dependencies, StravaToolbarActivity and HomeNavBarHelper both reside in the handset module. At this point, we had several options to consider if we wanted to move ChallengesActivity to a challenges module downstream of handset:

Break apart some dependencies. Perhaps we didn’t really need to extend StravaToolbarActivity and could instead choose composition over inheritance. Extract an interface from HomeNavBarHelper, defined it downstream and use an AbstractFactory to provide an implementation. Move all of the dependencies to downstream common modules first, then move ChallengesActivity.

Up to this point, we had been trodding along using some combination of the 3 approaches above. While this enabled us to apply cleaner architecture patterns and remove tightly coupled code, we were moving slowly and spent days just unraveling dependencies of dependencies of dependencies. This brings us to Guild Week 2, where we made a philosophical change to our modularization approach: rather than pulling feature code and dependencies to downstream modules, we decided to push feature code to upstream modules. This allows us to move working code out of handset more quickly and then decouple iteratively as the modules thin out.

As you can see in Diagram 4, we “flipped” feature modules above handset. Not only is it easier to modularize code out of handset, we can now realize gradle build speed optimizations and caching: if new development is confined to a feature module, the build will only need to compile that single module and not the 60% of our codebase in handset.

Diagram 4

We have just started down this path and are looking forward to build time decreases and our codebase decoupling as we move at a much faster pace. We’ll report back with metrics and analysis in our next modularization post.