Understanding Gradle Tasks. Learn how to create, use, and see the… | by Dmitrii Leonov | Nov, 2022

Learn how to create, use, and view results of tasks

image by author

You rarely can deal with Gradle Tasks directly, and most of the time, new Tasks 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 Through doFirst And doLast 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:

Code Snippet # 1

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:

Code Snippet #2

The above code is more descriptive and speaks for itself. As you can see, we are calling create() on the project TaskContainer Thing tasksafter 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 to settings.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 configures Project 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 TaskProviderand making a reference to a Task,

Code Snip #3

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.

Code Snippet #4

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?

Tasks 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:

  1. 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.
  2. SKIPPED Missed for some reason. Could be the reason – marked as a task enabled = false Excluded from the execution process in the body of the function or through command line arguments -x and some others.
  3. 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 Speciality
  4. FROM-CACHE – Tasks can be carried over from previous builds. uses a feature called task output caching, This is the advancement used in incremental builds UP-TO-DATE Because it can reuse remote cache by fetching from CI. unless you have org.gralde.caching=true In gradle.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,
  5. EXECUTED – The job has been executed successfully. This label is not displayed in the log.

To make these results visible, use the flag --console=plainFor 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?

Connect with me on Twitter and LinkedIn.

Leave a Reply