/ ANDROIDGRADLE
6 min read

Speed Up Your Android Project's Gradle Builds!

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

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

Build Profile

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 when upgrading your Gradle.

Gradle 5.0 Image from https://gradle.org/whats-new/gradle-5/

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

More on 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 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.

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

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

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.

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.

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

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.

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

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

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

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

More on 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.

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.

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

android {
    aaptOptions {
        cruncherEnabled false
    }
}

More on crunchPngs.

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.

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.

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

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

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.

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

More on Crashlytics Build Tools.

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.

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.

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.

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

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

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 Feature Modules and Google Play Instant App.

Here’s an article by Joe Birch regarding Modularization of Android Applications.

This is a detailed article how modularization can improve your Android app’s build time by Nikita Kozlov.

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.

Configure On Demand

You will only benefit from this configuration if you have a multi-project builds that have 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.

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.

Conclusion

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. 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.

Happy coding!

joshuadeguzman

Joshua de Guzman

Mobile Software Engineer at Freelancer.com, Passionate Volunteer at Flutter Philippines. Have a knack for learning and teaching. www.joshdeguzman.com

Follow