The gritty truth about app modularisation — part 2

Mark Ng
Mark Ng
Nov 8 · 6 min read

You can read part 1 here, but it’s not a prerequisite for this article.

Modularising an app is a mammoth task but just like everything else if you break it down into smaller more achievable tasks you can get anything done. Over the last 12 months, that’s exactly what we did and now we are getting many benefits from that work. One measurable benefit is build speed, we are now saving the equivalent of 700 hours a year which is equivalent to $70K annually or adding an extra developer to our team. This is the article that I said I wouldn’t write but since there aren’t many how-to articles on this topic I thought some of this information might be helpful to others.

To get there we had to complete 3 major steps which I refer to the first step, the leap of faith and the last step. To avoid confusion we refer to a module as a project in Android Studio and a feature as a business function that a product owner would talk about. During our modularisation journey, it’s interesting to note that we didn’t have to re-architect our app to get it done. Our app uses a very simple architecture with only a few layers.

Here are those steps;

First step

  1. Clean up our build scripts and make then reusable
  2. Centralize the version numbers for all dependencies
  3. Create a network module
  4. Create an initial base module
  5. Move non UI code to separate modules
  6. Move custom views to separate modules

After the first step, this got us 30 % of the way and after 6 months we had now gone from 3 modules to 32 modules. All the work done during this step was rolled into other features we were implementing at the time.

Leaf of Faith

  1. Move the base activity and base fragment to the base module
  2. Split the base module into multiple finer grain base modules
  3. Start moving UI code to modules
  4. Solve the navigation problem between modules with an interface

This was by far the most difficult step and after 3 months of coding, we were now 55% done. At this stage, we now had 45 modules and again we didn’t have any dedicated time to complete this work. One of the important things that we implemented during this stage was a navigation interface to decouple our UI dependencies.

We also changed our view binding back to findByView() since Kotlin synthetics doesn’t support using properties from another module and it was unlikely they were going to fix this defect. We took this opportunity to also removed Butterknife due to its lack of incremental annotation processing. We can’t wait for the new ViewBindings library which is coming out soon in Android Studio 3.6.

Last step

  1. Complete moving all UI code to modules
  2. Upgrade or remove non-incremental libraries such as Butterknife
  3. Move to DH3 for navigation
  4. Create dependency injection modules
  5. Create runner projects
  6. Clean up dependencies
  7. Clean up Gradle build scripts

Finally, we were in the home straight and after completing all the work above we were 90% done. This took us about 3 months to do and our final module tally stood at 90 modules. We still have a bit of clean up to do but we don’t expect that to take any more than a few weeks.

During this stage, we created the model module which consisted of POJOs, interfaces, and navigation, fixed our dependency injection and created the runner modules so we could run features separately.

Dependency injection

Up until the last stage, all the binding of our dependencies was done in the app module. In order to make our modules run independently, we created binding modules to define how our interfaces were implemented. These modules were then registered in the app and used by our runner modules. Using interfaces also allowed us to provide stub implementations when assembling our runner modules.

It's important to note that our app only uses 2 scopes (global, activity) to simplify our code. We use the Toothpick framework for DI as it provides everything we need and much simpler than Dagger 2.

Dart Henson 3

In a nutshell, the Dart Henson 3 library helps you simplify your navigation code using extra bindings and intent builders. Using annotation processing and Java reflection it generates a HensonNavigator class which can be used to navigate to other activities without including their dependencies. It was specifically designed to work with modules and a great alternative to the Jetpack’s Navigation component which is just too difficult to move to if your app is activity-based.

Dart Henson 3 requires you create a model module consisting of <youractivity>NavigationModel classes. These model modules can then be included as a dependency in your UI modules. The Dart Henson 3 Gradle processor then generates a Henson Navigator class which contains methods that create the intents to navigate to another activity.

Runners

Even after we had nearly fully modularized the project, moved a majority of our libraries to incremental annotation processing, upgraded to the latest version of Gradle and the Android Gradle plugin and switched over to the new Apply Run in Android Studio 3.5.0 we were still not satisfied with the speed of our builds. Our only option for improving our build speeds was to create special runners that could run a small portion of our app. So we created a runner for each of the tabs on our bottom navigation with the intent that each runner would only build and compile that functionality. Since we were only building roughly ~25 % of the code for each runner we saw a huge reduction in our build times.

A runner is essentially another app that launches a different activity than the main app. The runner should be signed by the main app’s Keystore so it is able to share session data. To use a runner, first you need to login and authenticate the main app so the session token created in shared preferences can also be used by the runner.

Evolution

During our modularisation journey, we were constantly learning new things and as we did we found that we needed finer grain modules. We created these modules out of need not because we thought we might need them later on.

Package splits

As you can see the number of base modules dramatically changed between stages 2 and 3.

We also found that some of our features had to be broken up more as they needed to be reused.

Gradle templates

If you are familiar with Maven and the concept of archetypes we found that we need more specialized build scripts that only pulled in the dependencies for that type of project.

This is just one example of how tracking feature in the app was split up to use the different types of Gradle builds files.

In the next article, I will share 11 tips & tricks that we used to get the job done and I will also show what our modules looked like after we had finished.

https://medium.com/@markchristopherng/the-gritty-truth-about-app-modularisation-part-3-2831fe5ba5ab

Thanks to karl

Mark Ng

Written by

Mark Ng

Android Platform Manager @Auspost

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade