Learn how to create, use, and view results of tasks
You rarely can deal with Gradle Tasks directly, and most of the time, new Task
s are linked either as a part of a plugin or as a copy-pasted piece of code from the libraries’ “how to start” guides. Having no understanding of how tasks work, their structure, and their lifecycle, developers are likely to shy away from adding any changes to those tasks, even when there is room for improvement.
Let’s try to understand in this article What are Gradle tasks, how are they created and used, and what forms can they take?
Basic things you should know about A Task
,
- a
Task
is an executable piece of code that consists of a sequence of actions. - actions are added to a
Task
ThroughdoFirst
AnddoLast
off. - A list of available functions can be accessed by executing
./gradlew tasks
,
For a better understanding of the structure, syntax, and relationships between Gradle project entities, see my previous post,
Further in the article, given that you are testing the code in Android project or any other project with Gradle wrapper, executing task X means running ./gradlew X
on mac or gradlew.bat X
on Windows.
As you may already know, things come in many different forms in Gradle, and tasks are no exception. Tasks can be defined in several different ways, typically like this:
while driving taskName1
From above, the output is not so clear:
> Configure project :app
Why is this printed first?
> Task :app:taskName
First?
Last?
But wait, it gets more confusing. Let’s add an alternate and more explicit form of the same function, just named differently:
The above code is more descriptive and speaks for itself. As you can see, we are calling create()
on the project TaskContainer Thing tasks
after which we configure the newly created group property Task
and add actions to the list.
let’s do our bit taskName1
See the output once again:
> Configure project :app
Why is this printed first?
Why is this printed first?
> Task :app:taskName
First?
Last?
As you can see, “Why is this published first?” Printed twice. So why is this happening, and where is it coming from?
Build lifecycle and stages
Unlike the functions declared above, most functions depend on each other. In order to execute a task, Gradle needs to understand the tasks available in the project and what the dependencies of the task are. For that, Gradle builds a directed acyclic dependency graph and executes tasks accordingly.
Don’t be shocked by the term directed acyclic dependency graph. It just means that:
- Tasks and their dependencies are built into a graph structure where nodes represent tasks, and vertices/lines represent dependencies.
- The direction of vertices shows how one task depends on other tasks.
- Acyclic means that there are no functions A and B where the two depend on each other either directly or transitively.
There are three stages of construction:
- initialization – starts with creating a
Settings
object according tosettings.gradle
Creates a hierarchy of sub-projects (referred to as modules in Android Studio) included in the file and Gradle project. - config – configures each project discovered in the initialization phase, then jumps to the relevant
build.gradle
files and configuresProject
Constructs a graph of instances and functions on which the target function directly or transitively depends. - Execute — Executes a task and all tasks depend on the executed task, which is known from the configuration step.
Knowing the build steps, we can now figure it out, although we don’t execute it taskName2
code directly inside configure
closures are still executed during the configuration phase, which causes Why is this printed first?
appearing twice.
Can it be avoided?
yes, for that, gradle has Configuration Avoidance API, This is the recommended way to create tasks that help reduce configuration time by avoiding doing Task
examples are using it directly and instead TaskProvider
and making a reference to a Task
,
using the TaskContainer.register()
Will prevent a task from being included in the configuration phase until the registered task is executed directly or is included in the dependency graph of the task being executed.
try to run taskName1
once again and see that the output is the same as it was before adding taskName3
, Also, running taskName3
Adds one more line to the configuration part of the log as it is now included in the configuration step all together taskName2
And taskName1
> Configure project :app
Why is this printed first?
Why is this printed first?
Why is this printed first?
> Task :app:taskName
First?
Last?
why do we have doFirst
and do?
Why is it not enough to just put the actions in the correct order of execution, and why do we need it doFirst
And doLast
,
For a moment, consider these closures as something to be executed before and after X. What is x, then?
To answer this, let’s define a simple task class and run the task of the newly created task class, demonstrating another way of defining a task.
Putting aside the configuration-related part of the log, the output for both functions of the newly defined type will be:
> Task :app:taskName5
First?
Before and after actions annotated with @TaskAction
Last?
then supplied the actiondoFirst
And doLast
are executed before and after actions annotated with @TaskAction
,
As you can see from code snippet #4, CustomTaskType
extends DefaultTask
class, a base class that you can extend to implement a custom task class. Gradle has a number of useful, ready-to-use task types that you can Search Gradle’s Github,
What else is there to know about Gradle Tasks?
Task
s are results that indicate what happened to the functions during the creation process. Intuitively, you can guess that a task can have three results – not executed, executed using cached results, and executed now.
In Gradle, there are five task outputs:
NO-SOURCE
– A task has not been executed because the input data required for its execution was not found. An example of input could be an annotated file@InputFiles
@SkipWhenEmpty
And@Incremental
Which failed to be produced by any prior work.SKIPPED
Missed for some reason. Could be the reason – marked as a taskenabled = false
Excluded from the execution process in the body of the function or through command line arguments-x
and some others.UP-TO-DATE
– A task result has not changed since the last creation and can be reused. it occurs as a part of incremental build SpecialityFROM-CACHE
– Tasks can be carried over from previous builds. uses a feature called task output caching, This is the advancement used in incremental buildsUP-TO-DATE
Because it can reuse remote cache by fetching from CI. unless you haveorg.gralde.caching=true
Ingradle.properties
or you use--build-cache
Flag while executing a task does not apply to your build. For a function to be catchable, it must be annotated as@CacheableTask
,EXECUTED
– The job has been executed successfully. This label is not displayed in the log.
To make these results visible, use the flag --console=plain
For example, in an Android project, you can use assambleDebug
,
./gradlew assembleDebug --console=plain
Ideally, a build should have as many UP-TO-DATE
And FROM-CACHE
To get faster execution time.
runtime and dependencies
It has been mentioned that a Task
Might depend on other functions, but how does this look in code? These indicators below show that tasks are interdependent:
Clearly define the relationship between the two functions:
dependsOn
,task X dependsOn Y
Task Y needs task X for its execution, and if X fails to execute, then Y will not execute.finalizedBy
,task X finalizedBy Y
Task Y will be executed after task X, even if X has failed to execute or has been abandoned.
Input and output annotations:
@OutputFile
And@InputFile
– There is an implicit way to create dependencies by annotating the inputs and outputs of functions. This approach requires configuring functions that have matching inputs and outputs.
Functions can be defined in many ways, but not all are equally good. For configuration time, use the configuration avoidance API and register tasks TaskContainer.register()
,
good to understand Task
Try to cache task execution results where possible by properly structuring dependencies between results and tasks and putting incremental builds and task output caching mechanisms to work to identify weak points of the build.
Want to Connect?