Every Android application contains some resources like localized strings, icons, screen
layouts, or navigation targets. And every native Android application accesses these resources
using Android’s retrieval mechanism based on resource IDs listed in R class. Let’s deep dive
into the world of almighty R to see whether there are any gotchas or possible improvements.
Spoiler alert: There are!
What is a R class?
Android application resources are placed in a directory structure under
res root (do not confuse that with Java application’s
resources directory) but may differ in a format. Icons and
layouts are placed in their individual files, but strings might (or might not) be stored all in
a single file and navigation targets are part of much richer content of navigation graphs.
Nevertheless, all application resources have an unique resource ID generated by
aapt tool during
compilation. All resource IDs are listed in a Java class with a simple name—R. For each resource
type, there is a nested class with the resource type name (e.g.,
R.string for string resources), and for each resource of that
type, there is a static integer (e.g.,
R.string.app_name).
This resource retrieval mechanism is heavily used in many parts of Android SDK and is optimized
for performance. Unfortunately, it is not optimized for developer happiness and safety. As all
resource IDs are of type
Int, one can easily use a string resource ID to retrieve an icon
which won’t end up well.
Let’s focus now on how the R class is generated and used by the rest of the application code.
The origins
Since the first public Android release in 2008, the principle of resource retrieval has stayed
the same. Before the application code gets compiled, all resources under the
res directory must be found, the R class source code is then
generated and merged with the rest of application source code. Once done, the application code
can reference resources IDs of R class like any other code.
At that time, it was the responsibility of Android Development Tools (ADT) Plugin to initiate
aapt, generate the
R.java source file, merge it with other application source files
and compile them together.
public final class R {
public static final class string {
public static final int cancel = 17039360;
public static final int copy = 17039361;
public static final int cut = 17039363;
public static final int no = 17039369;
public static final int ok = 17039370;
public static final int paste = 17039371;
public static final int yes = 17039379;
}
public static final class layout {
public static final int activity_list_item = 17367040;
public static final int list_content = 17367060;
public static final int preference_category = 17367042;
public static final int select_dialog_item = 17367057;
}
public static final class drawable {
public static final int btn_default = 17301508;
public static final int btn_dialog = 17301527;
public static final int btn_dropdown = 17301510;
public static final int btn_minus = 17301511;
public static final int btn_plus = 17301512;
public static final int checkbox_off_background = 17301519;
public static final int checkbox_on_background = 17301520;
public static final int divider_horizontal_bright = 17301522;
}
}
The first versions of appt were generating all resource IDs in R class as constants (public static final int in Java). This appears to be very bad for
build performance of modularized projects that have multiple library modules. Actual values of
resource IDs in these modules might collide and as a result all library modules had to be
recompiled more frequently. So from ADT version 14 the
library module’s R classes have their fields generated as
public static int (non-final), but the leaf application modules
kept their fields as
final since no other modules depend on them.
This change was quite an issue at that time since Java’s
switch statements require compile time constants and one cannot
use a library module resource ID as a statement value. But with Kotlin’s
when expression this isn’t an issue anymore.
Gradle times
At Google I/O 2014, Android Studio with Gradle-based build system was announced as a replacement
for ADT and Ant-based build. But since R class code generation is done by the aapt tool, not
much has changed in the way resource IDs are generated and consumed.
Elephant in the build
As projects grow, their R classes grow too. It was around 2017 when some bigger development teams
realized that the R classes grow much faster. Elin Nilsson in her
talk
about app modularization mentioned that in their 1,2 million LoC codebase the generated
R classes together would sum up to 56 million LoC that need to be compiled and dexed on every
clean build.
Wait, what? Do I actually have millions of unique resources in my modularized app? Of course not!
The main reason why the R classes are so huge is that they contain duplicates. R classes are
generated for every module of your build and the module specific R classes include references to
all resources of its transitive dependencies.
In this example, the MDC library contains
com.google.android.material.R class with references to material
resources.
Our
:lib module depends on the MDC library and contains few other
resources. It also has its own
com.example.myapp.lib.R class where references to its own
resources and transitive references to resources of MDC library are listed.
Finally, the
:app module has its own generated
com.example.myapp.R class and lists both
:lib module and MDC library reference IDs again.
Material Components library resources IDs are generated three times! And this is the story of how
a small modularized application can achieve few millions LoC in R classes.
The rescue
The Android Developer Tools team noticed this problem and rolled out a few tweaks that can help
to tame this monster. Let’s go through the list in the order in which they have been
released.
Android Gradle Plugin 3.3 (January 2019) introduces a not very well documented
flag you can enable in
gradle.properties:
android.namespacedRClass=true
It enables non-transitive R class namespacing where each library only contains references to its
own resources without pulling references from dependencies. This has a huge effect on R classes
size which leads to much faster builds.
As a bonus, it makes the module better isolated from an architectural point of view. Unless there
is an explicit import for the R class from another module, the module can use only its own
resources. It prevents the module from accidentally using a drawable or a string from another
module.
Unfortunately, Android Studio isn't aware of this so it won't prevent you from making
incorrect references, but at least it will fail at compile time.
This is all great (actually more than that!), but at the end of the day, the R class is just a
list of Ints known at compile time and you still need to compile it. Wouldn't it be nice if
AGP could generate Java bytecode directly? This dream comes true with another
gradle.properties flag:
android.enableSeparateRClassCompilation=true
This will make AGP to generate the R classes as compiled jar files instead of source code and
merge them with the rest of the project automatically. Technically, instead of
R.java file in
build/generated directory, the build process will generate
R.jar file in
build/intermediates directory.
This won’t speed up your build in the order of magnitudes, but it’s still better than nothing.
And of course, you can use any of these flags separately if it is too hard for you to enable
them together at once.
Android Gradle Plugin 3.4 (April 2019) doesn’t give us much but there is an
interesting paragraph in the release notes:
The correct usage of unique package names is currently not enforced but will become more
strict on later versions of the plugin. On Android Gradle plugin version 3.4, you can opt-in
to check whether your project declares acceptable package names by adding the line below to
your
gradle.properties file:
android.uniquePackageNames=true
There were no details for this requirement at that time, but having one package name used only in
one project module seems a good thing anyway, so why not enable this check right now and have no
migration issues in the future, right?
Android Gradle Plugin 3.6 (February 2020) gives the answer to the requirement
for a unique package name per module. Starting with this version, AGP simplifies the compile
classpath by generating only one R class for each module to speed up the build.
AGP 3.6 also makes the
android.enableSeparateRClassCompilation flag enabled by default
and it cannot be disabled anymore.
On the other hand, a new android.enableAppCompileTimeRClass
experimental flag was introduced. It is limited to Android application modules only.
Before a compilation phase of an application module can begin, all R classes from all
other modules need to be re-generated to create a final set of unique resource IDs that will be
used in the application module at runtime. This new experimental flag solves this
limitation by generating a fake application module R class in advance while updating it
with real resource ID values afterwards. One limitation of this approach is that the
application modules resource IDs cannot be final anymore (same as in
library modules), therefore they cannot be used in Java’s switch statements or as
annotation parameters.
Android Gradle Plugin 4.1 (release candidate in October 2020) makes another
step to make R
class namespacing enabled by default as it renames the flag from
android.namespacedRClass=true
to
android.nonTransitiveRClass=true
There is also a similar flag for app modules
android.experimental.nonTransitiveAppRClass=true
but according to the comments in AGP source code this seems to be just temporary and will be
removed in the future so you don’t have to bother now.
Conclusion
Access to app resources through R class has a long history full of slow builds.
If you are a conservative developer, just try to stay with the current stable version of Android
Gradle Plugin and things will get better over time. Sometimes you might be surprised with a new
requirement or package limitation and the list of changes above might help you to overcome
these.
If you are more progressive, just try the latest RC’s or betas and definitely try to enable
android.nonTransitiveRClass and
android.enableAppCompileTimeRClass
flags. It is a game changer and my guess is that it is the future for all of us anyway.
Pavel Švéda
Twitter: @xsveda