After Duplo modularization, we noticed that the task producing a transitive R class was taking a significant amount of time to execute. To eliminate this task altogether, and since the non-transitive R class is advertised to have up to 40% incremental build time improvement, we decided to migrate our codebase to use it.

If you’re not familiar with nonTransitiveRClass, previously known as namespacedRclass, it’s an Android Gradle Plugin flag that enables namespacing R classes so that every module’s R class only includes resources declared in the module itself, and not resources from the modules or libraries it depends on. This flag is enabled by default for new projects since Android Studio Bumblebee.

In this post, we’ll walk through how we transitioned to non-transitive R class and some of the benefits — both expected and unexpected — that we saw.

Establishing conventions

Kotlin

Since resources can’t all be referenced using the current module’s R class anymore, we first needed to align on how we wanted them to be referenced in the non-transitive R class world. Using the fully-qualified name for R classes to reference resources, e.g. getString(slack.l10n.R.string.at_everyone), was cumbersome and verbose, so we settled on using import aliases instead for that purpose:

import slack.l10n.R as L10nR

class Clazz(context: Context) {
  init{
    context.getString(L10nR.string.at_everyone)
  }
}

Java

Unfortunately, import aliases are a Kotlin-specific feature and not available in Java. However, considering that only 4% of our codebase was still in Java, we agreed that using the fully-qualified name to reference resources not declared in the current module was a good enough solution, and would serve as motivation to migrate those classes to Kotlin down the line.

Migration

Initial strategy

We were planning on using Android Studio’s Refactor > Migrate to Non-Transitive R Classes feature to refactor all R references to be fully-qualified, and then use this script to go through the refactored files and change the fully-qualified references to use import aliases.

However, at the scale of our codebase, with more than 400 modules, Android Studio took all night on an Intel Macbook Pro (we hadn’t yet moved to M1 Macs) to do part of the work and caused the UI to become non-responsive after, so we pivoted to a different migration strategy.

Chosen strategy

The majority of our resources are defined in two modules: the strings are in :l10n-strings and most other resources are in :slack-kit-resources. So, we proceeded with the following strategy:

  1. We first used Android Studio’s Find & Replace with the regex ([\[|(|]| )R\.string\. to replace string resource references with $1L10nR.string. and the regex ([\[|(|]| )R\.(color|dimen|drawable|font|raw|style|attr)\.sk_  to replace other resource references with $1SlackKitR.$2.sk_.
  2. Then we ran a modified version of the aforementioned script to iterate over files that used either L10nR or SlackKitR as a resource reference and added the necessary import aliases to them. The needed dependencies were added to the modules that were previously depending on either :l10n-strings or :slack-kit-resources transitively through the R class. The project started compiling successfully again.
  3. We enabled non-transitive R class by adding android.nonTransitiveRClass=true to the root gradle.properties file, and manually updated the few remaining resource references that were failing compilation.
  4. As the final step of the migration, we enabled a non-transitive R class dependent optimization that generates the compile time only R class using the app’s local resources by adding android.enableAppCompileTimeRClass=true to the same gradle.properties file.

Developer experience

Discoverability improvements

To help developers work as effectively in the non-transitive R class world as they did before, we found the four most common import aliases in the codebase, by running grep -o -h -r -E 'import \w+(\.\w+)+\.R as [a-zA-Z0-9]+R' . | sort | uniq -c | sort -b -n -r, and added them as live templates that start with r for discoverability.

Aliases live templates

Unlike file and code templates, live templates can’t be made available to everyone who works on the project through Intellij. To work around this, we copied the live templates file from Android Studio’s templates directory in ~/Library/Application Support/Google/AndroidStudio<version> and checked it into our Git repository under the config/templates directory. As a way to import it into Android Studio whenever a new version is installed, we then used this script, which runs as part of bootstrapping the local development environment, to copy the live templates file back to the templates directory.

Convention enforcement

In addition to the live templates, we also developed three lint checks, that built on top of the conventions that we agreed on, to enforced the following:

  1. Only the local module’s R class can be imported without an import alias.
    • For example, slack.uikit.resources.R can’t be imported outside :slack-kit-resources without an import alias.
  2. Import aliases for R classes should be consistent across the codebase.
    • For example, slack.uikit.resources.R can only be imported as SlackKitR, not SKR or anything else.
  3. R classes/resources can’t be referenced using their fully-qualified names, but rather through import aliases.
    • For example, getString(L10nR.string.at_everyone) has to be used instead of getString(slack.l10n.R.string.at_everyone).

All three lint checks have auto-fixes, so fixing any resulting lint issues is a breeze.

Benefits 

Migrating to non-transitive R class had many benefits, the most notable being a ~14% improvement in incremental build times following a resource or layout change.

It also reduced APK/DEX size by ~8.5%, which is about 5.5MB of code!

APK size difference graph

In addition to the benefits covered above, which were expected, the transition had some indirect benefits that were unexpected:

  • It uncovered instances where some resources were declared in one module but only used in another, which allowed us to move them for increased module cohesion. 
  • It made it easier to figure out where resources are coming from, which enabled us to narrow down inaccessible UI elements and triage them.

Conclusion 

All in all, the migration to non-transitive R class has been a big success for us, and developer sentiment is positive thanks to all its benefits and the tooling we implemented as support.

We recommend you make the transition to non-transitive R class to reap the benefits we’ve outlined if you are working on a multi-module project or plan on introducing modules to your project.

If you like working on stuff like this, we’re hiring!