skip to content

What is DefaultTask and how do you create a custom task type by subclassing it?

level: juniorimportance: must knowfreq 70%

answer

  1. Task interface vs DefaultTask base class
  2. @TaskAction = execution-phase entry point
  3. abstract class -> Gradle generates impl
  4. tasks.register(name, Type::class)
  5. constructor runs at config time, action at exec time

basics

~10 s

DefaultTask is Gradle's base class for tasks. You create a custom task type by subclassing it and adding one method annotated with @TaskAction, which holds the work the task runs when executed.

solid answer

~40 s

`DefaultTask` is the standard base class every custom Gradle task extends. To author a task type you subclass `DefaultTask` and put the task's logic in a single method annotated `@TaskAction`. Gradle invokes that method during the execution phase when the task runs. The class is typically declared `abstract` so Gradle can generate implementations for managed properties and inject services. You then register an instance with `tasks.register("greet", GreetTask::class)`. Properties on the task become inputs/outputs and configuration is done lazily via the registration block. Extending `DefaultTask` (rather than the raw `Task` interface) gives you the conventional implementations of dependency tracking, `dependsOn`, lifecycle, and logging out of the box.

code

kotlin · 13 lines
kotlin
abstract class GreetTask : DefaultTask() {
    @get:Input
    abstract val message: Property<String>

    @TaskAction
    fun run() {
        logger.lifecycle("Greeting: ${message.get()}")
    }
}

tasks.register("greet", GreetTask::class) {
    message.set("hello")
}

go deeper

for a junior

Know that DefaultTask is the base class and @TaskAction marks the method that does the work when the task runs.

for a middle

Explain config-time vs exec-time separation, why the class is abstract, and how registration realizes the task.

for a senior

Discuss managed properties, service injection via abstract getters, and laziness of register vs create.

for a principal

Frame conventions for a shared internal plugin: a base task class hierarchy, consistent action design, and how task authoring choices affect build maintainability across teams.

## What `DefaultTask` is In Gradle, a *task* is the unit of work (compile, copy, test, etc.). `Task` is the interface; `DefaultTask` is the concrete base class Gradle provides that implements all the standard plumbing — dependency tracking, `dependsOn`/`mustRunAfter`, the `Logger`, lifecycle state, and so on. When you author a **custom task type**, you subclass `DefaultTask` rather than implementing `Task` from scratch. ## The `@TaskAction` method A custom task class needs exactly one (or more) method annotated with `@org.gradle.api.tasks.TaskAction`. This method is the **entry point** Gradle calls during the *execution phase* when the task actually runs. Code in the constructor or in property initializers runs at *configuration time*; only the `@TaskAction` body runs at *execution time*. Keeping work inside `@TaskAction` is essential for configuration-time vs. execution-time separation. You *can* declare more than one `@TaskAction` method — Gradle runs them in declaration order — but idiomatic tasks use a single action method. ## Abstract task classes Modern Gradle (5+) encourages declaring task types `abstract`. Gradle generates a concrete subclass at runtime, providing implementations for `abstract` getters of managed properties (e.g. `abstract val message: Property<String>`) and enabling **service injection** through `@Inject` constructor parameters or abstract getters. You never instantiate the task with `new`; Gradle does it for you. ## Registering the type You add an instance to a project with the task container: ```kotlin tasks.register("greet", GreetTask::class) { message.set("hello") } ``` `register` is the lazy API — the task is only realized (configured/created) if something actually needs it. ## Minimal example ```kotlin abstract class GreetTask : DefaultTask() { @get:Input abstract val message: Property<String> @TaskAction fun run() { logger.lifecycle("Greeting: ${message.get()}") } } ``` ## Why subclass `DefaultTask` and not `Task` Implementing `Task` directly means re-implementing all the lifecycle and dependency machinery. `DefaultTask` gives it to you for free, so it is the documented, expected base for every custom task type.

  • What runs at configuration time versus execution time in a custom task?
    The constructor, field initializers and the registration/configuration block run at configuration time; only the @TaskAction method body runs at execution time when the task is actually invoked.
  • Why declare the task class abstract?
    So Gradle can generate the concrete implementation, supplying managed property getters (Property/ListProperty) and injected services declared as abstract getters or @Inject parameters.

saying these in an interview costs you the question

  • Saying the @TaskAction code runs at configuration time.
  • Claiming you should implement the Task interface directly instead of extending DefaultTask.
  • Instantiating the task with a constructor call instead of registering it in the task container.

context