Speed Up Your Android Project's Gradle Builds!
Tags: dev, android, gradle, kotlin
Published: 2019-06-10 10:21:42

## Introduction

Slower project build times may result in lower productivity. Lower productivity is lost money for the business. In this article, I provided a list of configurations, and tips you can implement for speeding up your Android project's Gradle builds. 

> DISCLAIMER: Some of the tips will include estimations, these estimations may not exactly match your project's build time improvement. Several factors affect the estimations -- including, but not limited to, your project's architecture, number of tests written, lint checks, current gradle configuration, your computer's hardware specifications, and the list goes on.

With that said, I hope this list will improve your overall team's productivity when building awesome apps!

## Improving Your Project Build Time

### 1. Profile, profile, profile!

**Profile your build, numbers don't lie.** As we may have different project architecture, tests, and way of writing code, it's best that you always **profile your own build** -- preferrably on each optimization configuration change you make. You can pass on `--profile` directly on the command-line when invoking Gradle task.

For example, `./gradlew assembleDebug --profile`.

Or, you can add it on the command-line options under Preferences when running the project on Android Studio.

![Command Line Options --profile](https://user-images.githubusercontent.com/20706361/59161162-90b50c80-8b11-11e9-8eea-1c341ce19403.png)

This is a sample report after running the task with `--profile`.

![Build Profile](https://user-images.githubusercontent.com/20706361/59161163-90b50c80-8b11-11e9-8b31-4a34374b55ae.png)

### 2. Always use the latest Gradle Plugin for Android

Gradle team release newer versions of Gradle with faster compilation and build times. At the time of writing, the latest release currently available is Gradle 5.4.1.

Note, newer version changes may introduce **unwanted compilation errors** or **compatibility issues**, make sure to check out and follow the [migration guide](https://docs.gradle.org/5.0/userguide/upgrading_version_4.html) when upgrading your Gradle.

![Gradle 5.0](https://blog.joshuadeguzman.me/assets/images/improve-gradle-build/step_1_gradle_5.png)
<small>Image from https://gradle.org/whats-new/gradle-5/</small>

Estimates that this will reduce 20-25% of your build time.

More on [Gradle 5](https://gradle.org/whats-new/gradle-5/) changes here.

### 3. Avoid Legacy Multidex

#### A brief introduction on multidex
Android build architecture has a limitation of **65,536 method references**, a.k.a. the **64K limit**, once a single [Dalvik](https://source.android.com/devices/tech/dalvik/) Executable or DEX file reached the limit, you will encounter a build error. **Multidex** was introduced to help you avoid the 64K limit. 

To support multidex, just add and set `multiDexEnabled` to `true`.

```groovy
android {
    defaultConfig {
        ...
        minSdkVersion 21
        targetSdkVersion 28
        multiDexEnabled true
    }
}

```

If your `minSdkVersion` is set to 20 or lower, then you must add the support library.

```groovy
android {
    defaultConfig {
        ...
        minSdkVersion 15
        targetSdkVersion 28
        multiDexEnabled true
    }

    dependencies {
        implementation 'com.android.support:multidex:1.0.3'
    }
}

```

However, this project setup will make your builds run with **legacy multidex**.

#### Legacy Multidex

Legacy multidex happens when you build your project with multidex enabled and `minSdkVersion` is set to 20 or lower. Clean and incremental builds are significantly slower on this. 

The simplest way to solve this is by creating a build variant for development. With this, you can now run an isolated build variant on your test device without worrying about legacy multidex.

```groovy
android {
    defaultConfig {...}
    buildTypes {...}
    sourceSets {...}
    productFlavors {
        // Create a separate build variant for development which has a minSdkVersion of 21
        dev {
            ...
            minSdkVersion 21
        }

        prod {...}
    }
}
```

Estimates that this will reduce 5-10% of your build time.

More on [multidex](https://developer.android.com/studio/build/multidex).

### 4. Disable lint checks on development

> NOTE: IMHO, I only recommend this step for a team that have computers with lower specs and does not have a strict requirement on lint checks during development.

If lint checks, especially if your project is large, take up at least 30% of your time **every build**, **think again**. You may only want to run your lint checks when you are creating a diff, such as running a script with lint checks specified as one of the tasks to execute before diff creation.

Option 1. Disabling lint check on `gradle.properties`. **Not recommended** when you are running builds in your **CI**, as you may want to **enable** lint checks for your **release builds**.

```
gradle=build -x lint
```

Option 2. When using Android Studio to run your project, pass `-x lint` in your command-line options under Preferences.

![x-lint](https://user-images.githubusercontent.com/20706361/59161158-901c7600-8b11-11e9-8f22-1e53a7fb21f3.png)

Option 3. Pass `-x lint` via command-line when executing a Gradle task.

For example, `./gradlew assembleDebug -x lint`.

### 5. Disable multi-APK generation

Google Play Store allows us to publish multiple APKs for specific device configuration. `splits` block allows us to configure the Multiple APK support.

This is a sample configuration for multi-APK support.

```groovy
android {
    splits {
        density {
            enable true
            exclude 'ldpi', 'xxxhdpi'
            compatibleScreens 'small', 'xlarge'
        }
    }
}
```

However, we don't need to generate multiple APKs during development.

```groovy
if (project.hasProperty('devBuild')) {
    // Prevent multi apk generation on development
    splits.abi.enable = false
    splits.density.enable = false
}
```

> Gradle executes the project's build file against the `Project` instance to configure the project.

Note, you have to add `-PdevBuild` to your command to trigger the block with property check of the project instance.

For example, `./gradlew assembleDebug -PdevBuild`.

Or, when you are using Android Studio to run your project, pass it in your command-line options under Preferences.

![pdevbuild](https://user-images.githubusercontent.com/20706361/59161157-8f83df80-8b11-11e9-9a0c-c338f2eca278.png)

Estimates that this will reduce 5-10% of your build time. 

More on [multiple APKs](https://developer.android.com/google/play/publishing/multiple-apks).

### 6. Include minimal resources

Avoid compiling **unnecessary resources** that you aren't testing. Including, but not limited to, additional language localizations, and screen density resources. 

For development, you can optimize your project build time by specifying one language resource or screen density.

```groovy
android {
    productFlavors {
        dev {
            ...
            resConfigs ("en", "xxhdpi")
        }
    }
}
```

### 7. Disable PNG crunching

Android performs **automatic image compression every time** you build your app regardless whether it is a release or debug build type. It helps reduce the size of your app by optimizing the images for release builds, but it will **slow down project build times** when you are on development.

*Update: It's available since Android Studio 3.0 Canary 5 release.*

PNG crunching is enabled by default for the release build and disabled by default for the debug build type.

```groovy
android {
    buildTypes {
        release {
            // Disables PNG crunching on RELEASE build type
            crunchPngs false // Enabled by default for RELEASE build type
        }
    }
}
```

For older versions of the plugin

```groovy
android {
    aaptOptions {
        cruncherEnabled false
    }
}
```

More on [crunchPngs](https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.BuildType.html).

### 8. Configure DexOptions wisely

Gradle provides you a DSL object for configuring dex options. One of the options that can be configured is `preDexLibraries`. You have the choice on whether you want your project to pre-dex libraries. Take note that this can **improve incremental builds**, but can **slow down your clean builds**.

If you wish to enable it, you can do this.

```groovy
android {
    dexOptions {
        preDexLibraries true
    }
}
```

Configure wisely based on your development workflow preference. You should disable when doing clean builds on your CI builds.

### 9. Use Crashlytics only when needed

Every build, Crashlytics **always generate unique build ID**. You can speed up your debug build by disabling the Crashlytics plugin.

```groovy
android {
    buildTypes {
        debug {
            ext.enableCrashlytics = false
        }
    }
}
```

Next, disable the kit at runtime for debug builds when initializing it.

```kotlin
val crashlytics = Crashlytics.Builder()
                .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
                .build()
Fabric.with(this, crashlytics)
```

But if you need to use Crashlytics on debug build, you can still improve your incremental builds by preventing it from updating app resources with its own unique build ID every build.

```groovy
android {
    buildTypes {
        debug {
            ext.alwaysUpdateBuildId = false
        }
    }
}
```

More on [Crashlytics Build Tools](https://docs.fabric.io/android/crashlytics/build-tools.html).

### 10. Use static dependency versions

You must declare static or hard-coded version numbers for your dependencies in your `build.gradle`. **You should avoid** dynamic dependencies, represented by using plus sign (+), otherwise you might encounter unexpected version updates. Dynamic dependency declarations also **slower your builds** as it continue checking for updates online.

```groovy
android {
    dependencies {
        // What you should not do
        // implementation "androidx.paging:paging-runtime:2.+"

        // What you should do
        implementation "androidx.paging:paging-runtime:2.1.0"
    }
}
```

### 11. Enable build caching

**By default, build cache is not enabled**. To use build cache, you can invoke it by passing `--build-cache` on the command-line when executing a Gradle task.

For example, `./gradlew assembleDebug --build-cache`.

You can also configure build caching on your `gradle.properties` file.

```groovy
org.gradle.caching=true // Add this line to enable build cache
```

Estimates that this will improve your full clean build by up to 3 times faster, and incremental builds by up to 10 times faster!

### 12. Configure the heap size for the JVM

Gradle daemon now starts with 512MB of heap instead of 1GB. By default, this configuration may work best for a smaller project. But if you have a **large project**, you should update the heap size by setting `org.gradle.jvmargs` property in your `gradle.properties` file.

Here is the default configuration.
```
org.gradle.jvmargs=-Xmx512m "-XX:MaxMetaspaceSize=256m"
```

If you want to update the heap size to 2GB for larger projects.

```
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
```

Bonus, you need to set the right `file.encoding` properties when the JVM executes the Gradle build (eg. Gradle Daemon boot up). In this example, we are setting it to UTF-8.

More on [Gradle's default memory settings](https://docs.gradle.org/current/userguide/upgrading_version_4.html#rel5.0:default_memory_settings).

### 13. Modularize your project

Modularizing your project codebase allows the Gradle build system to **compile only the modules you modify** and cache those outputs for future builds.

Here's a quick overview of project modularization.

![Modularization Sample 1](https://user-images.githubusercontent.com/20706361/59161159-901c7600-8b11-11e9-81c0-7beeeef3ab8c.png)

How the build system compiles your project based on module specific changes.

![Modularization Sample 2](https://user-images.githubusercontent.com/20706361/59161160-901c7600-8b11-11e9-9997-79a47f7f0692.png)

I recommend modularization not only because of module re-use, or for optimizing build times, but also because it is ready to support the latest **Android's Dynamic Features** and **Instant App**.

[Here's](https://medium.com/google-developer-experts/modularizing-android-applications-9e2d18f244a0) an article by Joe Birch regarding Modularization of Android Applications.

[This](https://www.freecodecamp.org/news/how-modularisation-affects-build-time-of-an-android-application-43a984ce9968/) is a detailed article how modularization can improve your Android app's build time by Nikita Koslov.

### 14. Runners-up: Always-on daemon, parallel build execution, and configure on demand.

#### Gradle Daemon

By default Gradle Daemon is enabled, but if the current project you are managing disabled the daemon for the every build and it annoys you -- you might want to update the configuration by enabling it.

```
org.gradle.daemon=true
```

#### Parallel Build Execution

This configuration is effective only when your project is modularized. It will allow you to fully utilize the processing resources you have in your computer.

```
org.gradle.parallel=true
```

More on [parallel execution](https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:parallel_execution).


#### Configure On Demand

You will only benefit from this configuration if you have a multi-project builds that have [decoupled projects](https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:decoupled_projects) -- take note, it's multi-project, not just a modularized project.

Let's make an example out of the Google I/O 2018 Android App.

![Google I/O 2018 Android App](https://user-images.githubusercontent.com/20706361/59161161-90b50c80-8b11-11e9-917d-fdd5bf6d1c93.png).

Notice the project structure, it includes various projects such as `mobile`, and `tv`. Configure on demand will attempt to configure only the projects that are relevant for the requested tasks (eg. **configure only** `mobile` for the requested task).

> NOTE: The configuration on demand feature is incubating so not every build is guaranteed to work correctly.

More on [configure on demand](https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:configuration_on_demand).

### Conclusion

<mark>Keep your team updated with the latest Gradle releases, and lookout for the possible API deprecation. Always profile your build, and adjust the configurations accordingly based on the results you get.</mark>

With these tips, I hope you will significantly improve your team's productivity and help each other make great apps again!

If you have questions or suggestions, feel free to comment below or ask me on my twitter [@devjdg](https://twitter.com/devjdg).

Happy coding!