How do you define a custom task in build.gradle.kts, and what is the difference between tasks.register and tasks.create?
answer
- register = lazy, returns TaskProvider
- create = eager, returns the Task
- Configuration avoidance saves config-phase time
- named<T>() to configure existing tasks lazily
- doLast/doFirst add actions; dependsOn orders
basics
~10 sYou define a task with tasks.register("name") { ... }, configuring it inside the receiver block. register creates the task lazily (only when needed), while create makes it eagerly every build, which is slower.
solid answer
~40 sIn the Kotlin DSL you register a task with tasks.register("name") { ... } or the typed tasks.register<Copy>("name") { ... }, configuring it inside a receiver block where this is the task. tasks.register returns a TaskProvider and is part of the configuration-avoidance API: the task is created and configured lazily, only if it ends up in the task graph. tasks.create is the older eager API: it instantiates and configures the task immediately during configuration, even if the task never runs, which hurts configuration-time performance on large builds. Best practice is register plus configuration avoidance everywhere (tasks.named<T>("x") to tweak existing tasks). Inside the block you wire inputs/outputs and add actions via doLast { } / doFirst { }; with the typed overload you can set type-specific properties (e.g. a Copy task's from/into).
code
kotlin · 15 lines// Lazy registration with a typed task:
val copyDocs = tasks.register<Copy>("copyDocs") {
from("src/docs")
into(layout.buildDirectory.dir("docs"))
}
// Lazy configuration of an existing task + wiring via the provider:
tasks.named("build") {
dependsOn(copyDocs)
}
// Eager (avoid on large builds): created and configured even if never run
tasks.create("legacyHello") {
doLast { println("hi") }
}
go deeper
Can register a simple task with a doLast action and knows register is preferred.
Explains lazy vs eager, TaskProvider, and configuration avoidance; uses named for existing tasks.
Connects eager creation to configuration-phase cost on large multi-module builds and wires tasks via providers and inputs/outputs.
Sets team conventions enforcing register/named and reasons about build-time scalability and configuration-cache compatibility.
## Defining a task A *task* is a unit of build work. In the Kotlin DSL: ```kotlin tasks.register("hello") { group = "custom" description = "Prints a greeting" doLast { // an action that runs at execution time println("Hello from Gradle") } } ``` - `register("name") { ... }` — the lambda is a **receiver block** where `this` is the task being configured, so you set `group`, `description`, inputs/outputs, and actions directly. - **Typed overload** lets you create a task of a specific type and use its properties: ```kotlin tasks.register<Copy>("copyDocs") { from("src/docs") into(layout.buildDirectory.dir("docs")) } ``` ## register vs create — the key distinction | | `tasks.register(...)` | `tasks.create(...)` | |---|---|---| | Returns | `TaskProvider<T>` | `T` (the task) | | Timing | **Lazy** — created/configured only if needed | **Eager** — created/configured immediately | | Part of | Configuration-avoidance API | Legacy eager API | | Performance | Cheap on large builds | Pays cost every build, even if task never runs | **Configuration avoidance**: Gradle splits a build into a *configuration phase* (evaluate scripts, build the task graph) and an *execution phase* (run the selected tasks). `register` defers a task's creation/configuration until something actually realizes it (it's requested, or a dependency pulls it in). `create` runs that work during the configuration phase unconditionally. On a multi-module build with hundreds of tasks, eager creation noticeably slows every invocation, even `gradle help`. ## Configuring existing tasks lazily To tweak a task a plugin already registered, use the lazy accessor rather than realizing it: ```kotlin tasks.named<Test>("test") { useJUnitPlatform() maxParallelForks = 4 } ``` `named` returns a `TaskProvider` and configures lazily; `getByName`/`tasks.test` (the eager accessor) realize the task immediately and should be avoided in hot paths. ## doLast / doFirst and dependencies - `doLast { }` appends an action run last; `doFirst { }` prepends one. - Order tasks with `dependsOn(...)`, `mustRunAfter(...)`, `finalizedBy(...)`. - For correctness and up-to-date checks, declare `inputs`/`outputs` so Gradle can skip the task when nothing changed. ## Wiring providers Because `register`/`named` return `TaskProvider`, you can wire tasks together without realizing them: ```kotlin val gen = tasks.register("generate") { /* ... */ } tasks.named("compileKotlin") { dependsOn(gen) } ``` ## Rule of thumb Prefer **`register` + `named`** everywhere; reach for `create`/`getByName` only when forced by an old API. This is officially the recommended style for modern Gradle.
- What does tasks.register return, and why is the return type useful?It returns a TaskProvider, a lazy handle to the (possibly not-yet-created) task. You can pass it to dependsOn or wire its outputs without forcing the task to be created, preserving configuration avoidance.
- When configuring a plugin-provided task like test, why prefer tasks.named over tasks.getByName?named configures lazily through a TaskProvider, so the task is only realized if needed; getByName eagerly realizes it during configuration, defeating configuration avoidance.
register is ordering food only when a customer actually asks; create is cooking every dish on the menu at opening whether or not anyone orders it.
saying these in an interview costs you the question
- Saying register and create are interchangeable with no performance difference
- Thinking register returns the Task object rather than a TaskProvider
- Using tasks.create everywhere by habit on a large build
- Realizing tasks eagerly (getByName) when named would do
- Not knowing the configuration vs execution phase split