Gradle is a powerful build automation tool used in JVM-based projects like Java, Kotlin, and Groovy. Understanding its task lifecycle is essential for writing efficient and well-structured build scripts. This article explains the Gradle task lifecycle, execution phases, plugin tasks, and the role of doLast {} in defining main actions.


Gradle Task Lifecycle

A Gradle build goes through three main phases:

1. Initialization Phase

  • Identifies the projects involved in the build.
  • Creates Project objects but does not execute tasks.

2. Configuration Phase

  • Evaluates build.gradle or build.gradle.kts scripts.
  • Defines tasks and their dependencies, but does not execute them.

3. Execution Phase

  • Determines which tasks need to run based on dependencies.
  • Executes each task in the correct order.
  • Each task runs in three steps:
    1. doFirst {} (Pre-action, runs before the main task logic)
    2. Main task action (Defined within doLast {} if no type is set)
    3. doLast {} (Post-action, executes after the main task logic)

Execution Phase Example

Let's define some simple tasks and observe their execution order:

// Define taskA
task taskA {
    doFirst { println "Before taskA" }
    doLast { println "Main action of taskA" }
    doLast { println "After taskA" }
}

// Define taskB
task taskB {
    doFirst { println "Before taskB" }
    doLast { println "Main action of taskB" }
    doLast { println "After taskB" }
}

// Define taskC, which depends on taskA and taskB
task taskC {
    dependsOn taskA, taskB
    doLast { println "Main action of taskC" }
}

Expected Output when Running "gradle taskC"

> Task :taskA
Before taskA
Main action of taskA
After taskA

> Task :taskB
Before taskB
Main action of taskB
After taskB

> Task :taskC
Main action of taskC

Since taskC depends on taskA and taskB, Gradle ensures that taskA and taskB execute before taskC.


Common Main Task Actions

Gradle tasks can perform various actions, such as:

Compiling code (compileJava)

task compileCode {
    doLast { println "Compiling source code..." }
}

Copying files (Copy task)

task copyFiles(type: Copy) {
    from 'src/resources'
    into 'build/resources'
}

Running tests (test task)

task runTests {
    doLast { println "Running unit tests..." }
}

Creating a JAR file (Jar task)

task createJar(type: Jar) {
    archiveBaseName.set("myApp")
    destinationDirectory.set(file("$buildDir/libs"))
}

Running an application (JavaExec task)

task runApp(type: JavaExec) {
    mainClass = "com.example.Main"
    classpath = sourceSets.main.runtimeClasspath
}

✅ Cleaning build directories (clean task)

task cleanBuild {
    doLast {
        delete file("build")
        println "Build directory cleaned!"
    }
}

Are Plugin Tasks Part of the Main Task?

  • No, plugin tasks do not run automatically unless explicitly executed or added as dependencies.
  • Applying a plugin (e.g., java) provides tasks like compileJava, test, and jar, but they must be invoked or referenced.

Example:

apply plugin: 'java' // Adds Java-related tasks

task myBuildTask {
    dependsOn 'build' // Now includes plugin tasks
    doLast { println "Custom build complete!" }
}

Running gradle myBuildTask executes Java plugin tasks (compileJava, test, jar, etc.) before myBuildTask.


Do You Need doLast for the Main Task?

  • If a task has no type, the main action must be inside doLast {}.

    task myTask {
      doLast { println "Executing my task!" }
    }
  • If a task has a type, it already has built-in behavior, so doLast {} is only needed for additional actions.

    task copyFiles(type: Copy) {
      from 'src/resources'
      into 'build/resources'
    }

Avoid Running Actions Outside doLast

task badTask {
    println "This runs during the configuration phase!"
}

Problem: The message prints immediately during configuration, not when the task executes.

Solution: Use doLast {}.


Final Takeaways

✅ Gradle tasks go through Initialization → Configuration → Execution phases.

Tasks without a type need doLast {} for their main logic.

Plugin tasks are independent but can be linked via dependencies.

✅ Use built-in tasks (e.g., Copy, Jar, JavaExec) when possible.

✅ Always place executable logic inside doLast{} for tasks without predefined behavior.

By understanding these concepts, you can write efficient Gradle scripts that optimize build processes. 🚀