Allows you to get all performance and feature improvements. You can set up shadows jobs to help test against upcoming versions and catch any regressions in advance.
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.
Lazy configuration, callbacks, and provider chains are the name of the game.
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).
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 lets 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.
Use ValidatePlugins
that is added by java-gradle-plugin
and set
tasks.withType<ValidatePlugins>().configureEach {
failOnWarning.set(true)
enableStricterValidation.set(true)
}
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.
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.
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 initialized eagerly. Use configureEach
instead.
Apply order can be arbitrary, instead use pluginManager.withPlugin()
to reach when plugins are added.
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.
Sadly, Gradle default is to not 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.
- 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
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.
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.
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.
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
.
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.
Do not use task.outputs.upToDateWhen
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.
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.
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.
Use domain object containers instead. Once again, Gradle is able to provide enhanced DSL support this way.
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 gradually until you can resolve the problem.