Skip to content

liutikas/gradle-best-practices

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 

Repository files navigation

Best Practices when using Gradle

General

Use the latest Gradle and plugin versions

Allows you to get all performance and feature improvements. You can set up shadows jobs to help test agaist upcoming versions and catch any regressions in advance.

Don't use internal APIs

Gradle and many plugins (such as AGP) consider internal APIs fair game for making breaking changes in even minor releases. Therefore, using such an API is inherently fragile and will lead to major, completely avoidable, headaches. If you really need some functionality, it is often better to copy relevant bits over to your codebase.

Avoid making any ordering assumptions of any kind

Lazy configuration, callbacks, and provider chains are the name of the game.

Avoid afterEvaluate

It introduces subtle ordering issues which can be very challenging to debug.

What you're looking for is probably a Provider or Property (see also lazy configuration).

Create custom tasks

Gradle documentation suggests to use generic tasks with doFirst or doLast to do the work in - DON'T!. Even for simple tasks it is much better to create a custom task class as it let's you specify inputs, outputs, and most importantly cacheability of the task (see cacheability section).

abstract class MyTask: DefaultTask() {
    @get:InputFiles
    abstract val thingsToRead: ConfigurableFileCollection
    @get:OuputFile
    abstract val placeToWrite: RegularFileProperty
    @TaskAction
    fun doThings() = TODO()
}

Note, that making task and input/output properties abstract, Gradle will automatically initialize them for you without having to call project.objects factory methods.

Enable stricter Gradle plugin validation

Use ValidatePlugins that is added by java-gradle-plugin and set

tasks.withType<ValidatePlugins>().configureEach {
    failOnWarning.set(true)
    enableStricterValidation.set(true)
}

Dependencies

Keep your dependencies clustered

Having dependencies {} block with the dependencies clustered by destination (main, test, androidTest, etc.) makes it easier for others to see what's on the classpath and how it should be changed.

Use appropriate configurations for dependencies

Prefer implementation over api. Add dependencies in places you use it instead of adding to all projects or configurations just in case. Use dependency-analysis-android-gradle-plugin to help maintain clean dependency lists.

Use version catalogs for shared dependencies

Avoids having to change dozens of lines when you want to upgrade a version of a library. Reduced variation between versions used in the projects can also help have more accurate test coverage.

Laziness

Don't do expensive computations in the configuration phase

It slows down the build. Such computations should be encapsulated in a task action.

Avoid the create method on Gradle's container types

Use register instead.

Avoid the all callback on Gradle's container types

These cause object to be intialized eagerly. Use configureEach instead.

Don't assume your plugin is applied after another

Apply order can be arbitary, instead use pluginManager.withPlugin() to reach when plugins are added.

Don't call get() on a Provider outside a task action

The whole point of using a provider is to evaluate it as late as possible. Calling get() — evaluating it — will lead to painful ordering issues if done too early. Instead, use map or flatMap.

Cacheability

Make all tasks and transforms cacheable (with some exceptions)

Sadly, Gradle default is to not to cache any tasks or transforms. Use @CacheableTask and @CacheableTransform. The exceptions are:

  • copy/package(jar/zip)/unpackage(extract) since generally it is faster to rerun this task locally than downloading/unpacking it from the cache.
  • input is non-stable (time, git sha, etc) as you will have little to none cache hits.

Annotate your inputs and outputs

  • File inputs should be annotated as @InputFile or @InputFiles otherwise Gradle will not keep track on when these files change and your task is out of date.
  • Annotate properties that Gradle should not consider in task up to dateness with @Internal

Don't access a Project instance inside a task action

It breaks the configuration cache, and will eventually be deprecated. Instead, specify the exact inputs you were using Project instance for as an explicit task input.

Don't access another project's Project instance

This is called cross-project configuration and is extremely fragile. It creates implicit, nearly un-modelable dependencies between projects and can only lead to grief. Instead, share artifacts across projects by declaring dependencies.

It also breaks the experimental project isolation feature, but that won't be truly relevant for a while.

No overlapping output files and directories between tasks

Two tasks having the same output file or directly will likely result in constant build cache invalidation. Instead set unique outputs for every task, especially when creating per variant/flavor tasks.

Prefer @PathSensitive(PathSensitivity.NONE) for all file inputs

Sadly, Gradle default is to treat every file input as absolute path sensitive input. Instead, use @PathSensitive(PathSensitivity.NONE) as that let's Gradle know that you only care about the contents of the file and not their location. Other reasonable normalizers are PathSensitivity.NAME_ONLY, PathSensitivity.RELATIVE, or using @Classpath.

Make your tasks outputs deterministic

Consider sorting your inputs in a way that you have deterministic output for the same set of inputs. For example, this can come up when doing directory traversal or receiving non-ordered collections.

This API predates proper Gradle input/output handling, use annotations instead. The only reasoanble usage of this API is task.outputs.upToDateWhen { false } for tasks that should always re-run, but ideally you have very few of those.

Plugin public APIs (DSL)

Use plugin extensions to define your public API

Instead of using Gradle, system, or Java properties create Extension objects using extension container. This will allow your users have a robust way of configuring your plugin.

Don't use Kotlin lambdas in your public API

I know, it's tempting. They're right there. Use Action<T> instead. Gradle enhances the bytecode at runtime to provide a nicer DSL experience for users of your plugin.

Don't use lists in your custom extensions

Use domain object containers instead. Once again, Gradle is able to provide enhanced DSL support this way.

Testing

Run your integration tests with --warning-mode=fail

Making all warnings fail the builds under test by passing --warning-mode=fail to all your integration tests make sure your plugins don't use any deprecated Gradle features. It prevents you and contributors from inadvertently introducing such usages. Moreover, when you add a new Gradle version to your testing matrix you get direct feedback on what needs attention.

In the event you need to relax this for some test, do it granularly until you can resolve the problem.

Credits

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published