Way back in the early-ish days of Android we used the support libraries, android.support
, to backport certain features to different API levels. These days those libraries have been replaced by the AndroidX packages, announced at I/O 2018 - but their legacy lives on and many of the dependencies that your app consume may still depend on these android.support
packages.
Unfortunately, these two packages don’t play well together, so the Android team created a clever solution - Jetifier.
Jetifier is simple in concept: all it does is replace android.support
classes with their equivalent androidx
versions. As a simple example:
import android.support.v7.widget.AppCompatImageView
becomes:
import androidx.appcompat.widget.AppCompatImageView
This step happens while compiling the app assuming that you have android.enableJetifier=true
in your gradle.properties
file. It’s also worth pointing out that this happens in packaged artifacts, not in your source code. It’s still up to you to migrate your project itself, but Jetifier sorts third-party libraries for you by re-writing their binaries (Android Studio does also have a “Migrate to AndroidX” option, which takes some of the pain out of migration for you).
The downside: this is an extra build step in an already complex build pipeline. “Jetifiying” large dependencies can take a fair bit of time - perhaps a couple of seconds per binary. Worse, the outputs of this transform step are not cacheable, resulting in slower incremental builds every single time and this all adds up over the course of many compilations during a typical work day. That being said, this issue is marked as fixed in AGP 4.1.0-alpha01.
Jetifier also imposes a hit to Gradle’s configuration time - something that we’re already acutely aware of at Cuvva due to our large (150+) number of modules.
So it’s clear that there’s some build performance gains to be had by disabling Jetifier. What are the next steps?
How do I check if I can disable it?
Thankfully there’s a handy Gradle plugin which can help you work out whether or not you’re in a position to drop Jetifier, and point out the dependencies which are preventing you from doing so. The appropriately named Can I Drop Jetifier is easy to install and run:
plugins {
id "com.github.plnice.canidropjetifier" version "0.5"
}
./gradlew -Pandroid.enableJetifier=false canIDropJetifier
If you still have android.support
dependencies you’ll see output like this:
Cannot drop Jetifier due to following module dependencies:
* sample-dependency (module)
\-- com.android.support:cardview-v7:28.0.0
\-- com.squareup.leakcanary:leakcanary-android:1.6.3
\-- com.android.support:support-core-utils:26.0.0
\-- com.squareup.leakcanary:leakcanary-android:1.6.3
\-- com.squareup.leakcanary:leakcanary-analyzer:1.6.3
\-- com.android.support:support-annotations:28.0.0
Cannot drop Jetifier due to following external dependencies:
* com.android.support:cardview-v7:28.0.0
* com.squareup.leakcanary:leakcanary-android:1.6.3
\-- com.squareup.leakcanary:leakcanary-analyzer:1.6.3
\-- com.android.support:support-annotations:28.0.0
\-- com.android.support:support-core-utils:26.0.0
At this point you have two choices: update if you can and the library has recently been upgraded to AndroidX, or removing the dependency completely. You might find that it’s helpful to run this on a per-module basis as the output in a multi-module project can be quite large!
In our case, we found that Robolectric 4.3.1
, an outdated version of Stripe and a couple of other libraries were causing issues - thankfully all of these were easily fixable.
What if I can’t fix it?
It’s entirely possible that you might find dependencies that you rely on which aren’t yet upgraded to AndroidX, in which case you could end up a bit stuck. Thankfully there is a solution, although it’s non-trivial.
Jetifier can be run from the command-line on specific AAR
files, generating a transformed version that you can then import directly. The Jetifier docs, cover this usecase, but it’s essentially just:
./jetifier-standalone -i <source-library> -o <output-library>
Huge thanks to Rafa Garcia for pointing this option out!
Interestingly this tool also supports a reverse mode, so that library maintainers can offer both AndroidX and android.support
artifacts trivially.
How do you measure the difference?
To check to see what a difference disabling Jetifier made, we made use of Gradle Profiler. Getting set up isn’t too painful and this tool is hugely powerful, but the basics are easy. You’ll want to clone the repo and run the install command to generate the profiler binary:
git clone https://github.com/gradle/gradle-profiler
&& cd gradle-profiler
&& ./gradlew installDist
Next, add the resulting binary to your path or be lazy like I did and cd
into its output directory /build/install/gradle-profiler/bin
, and then in its most basic form, start profiling:
./gradle-profiler --benchmark --project-dir <~/your/project/dir> assembleDebug
This will run 6 warmup and 10 normal builds before creating a nice HTML document output with max, min and mean build times in ms
. We found some pretty inconsistent results for the warmup build times, but the “warm” builds were very repeatable.
gradle-profiler
is worth its own blog post and it’s wildly customisable - you can recreate Android Studio syncs, make changes in specific files and create complex configurations for repeatable benchmarks. We’d love to automate this at somepoint, but we haven’t decided exactly what we want to keep track of yet. Sakis at Babylon Health wrote an excellent blog about their setup here, which is a setup to aspire to.
What impact did we see?
Using gradle-profiler
, we saw reductions of up to 40% for incremental builds, which is a fantastic improvement for a minimal amount of work. For us, we only had a handful of dependencies left with android.support
imports remaining, the most painful of which to sort was Robolectric. But the afternoon I spent scratching my head was more than worth it in the end (plus, it’s made me realise that we’ve leant on Robolectric a little too readily, and we’ve already formulated a plan to remove it).
Given how many times per day each developer on the team runs a build, this results in a huge number of saved person-hours over time. If you really want to feel bad, you could use Build Time Tracker to see just how long per-day you spend waiting for builds and then extrapolate what a 40% saving might mean for you. I personally have never been so masochistic as to find out.
What’s more is this effect compounds - a ten second incremental build might be fine, but a 20 second build is long enough that I might get distracted and go down a Reddit rabbit-hole. This is also a good argument for those pushing for more powerful build machines. Your self control may be better than mine, though.
I highly recommend spending an afternoon looking into whether or not Jetifier is required for your build and then disabling it if possible. Please do profile your builds and I’d love to hear what the difference was for your project.
Lastly, if you found this interesting, we’re hiring.