How do the kotlin { } / java { } extension blocks and task configuration work as Kotlin lambdas-with-receivers, and how do you configure or register a task type-safely in the Kotlin DSL?
answer
- Blocks = lambdas with receiver (T.() -> Unit)
- tasks.named<Type>("n") to configure existing
- tasks.register<Type>("n") to create lazily
- withType<Type>().configureEach for all
- Lazy Provider/Property -> use .set(...)
basics
~20 sBlocks like kotlin { } and java { } are Kotlin functions whose argument is a lambda that runs against a typed object, so inside the braces you set its properties directly. To configure a task safely you use tasks.named<Type>("name") { ... } and to create one tasks.register<Type>("name") { ... }.
solid answer
~40 sConfiguration blocks in the Kotlin DSL are **lambdas with receiver**: `java { ... }` is really a method that takes an `Action<JavaPluginExtension>`, so inside the braces `this` is the typed extension and you assign its properties (`sourceCompatibility = ...`, `toolchain { ... }`). The same shape applies to plugin extensions like `kotlin { jvmToolchain(21) }`. For tasks, prefer the **lazy, typed** APIs: `tasks.register<Jar>("myJar") { archiveBaseName.set("x") }` creates a task only when needed (configuration avoidance), and `tasks.named<Test>("test") { useJUnitPlatform() }` configures an existing one with the correct receiver type so you get autocompletion. Use `withType<KotlinCompile>().configureEach { ... }` to configure all tasks of a type lazily. Avoid eager `tasks.getByName(...)`/`task(...)` and Groovy-style string property access; the typed generic variants give compile-time checking and keep configuration lazy, which matters for large builds.
code
kotlin · 15 lines// configure existing task, typed + lazy
tasks.named<Test>("test") {
useJUnitPlatform()
}
// register a new task lazily
tasks.register<Zip>("docsZip") {
archiveFileName.set("docs.zip")
from("build/docs")
}
// configure all tasks of a type
tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
}go deeper
Can put settings inside java {} / kotlin {} blocks and knows tasks have configuration blocks.
Knows tasks.named<>()/register<>() and that blocks configure a typed receiver object.
Explains lambda-with-receiver, configuration avoidance, withType().configureEach, and Provider/Property .set().
Drives lazy-configuration and convention-plugin standards so large multi-module builds stay fast and type-safe.
## Lambdas with receiver — the core mechanism Kotlin supports a **function type with receiver**: `T.() -> Unit`. When you call a function whose last parameter has this type, the lambda body executes with an instance of `T` as its implicit `this`. Gradle's `java { }`, `kotlin { }`, `dependencies { }`, and `tasks { }` are all such calls. ```kotlin java { // receiver is JavaPluginExtension sourceCompatibility = JavaVersion.VERSION_21 // property of the receiver toolchain { // nested receiver block languageVersion.set(JavaLanguageVersion.of(21)) } } ``` Because the receiver type is known at compile time, the IDE autocompletes its members and the compiler checks every assignment. (Under the hood Gradle uses `Action<T>`, which Kotlin treats as a receiver lambda.) Many Gradle types use the **Provider/Property** API, hence `.set(...)` instead of `=` on lazy properties like `languageVersion` or `archiveBaseName`. ## Configuring an existing task — type-safely Use the **generic, lazy** lookups so the receiver is the real task type: ```kotlin tasks.named<Test>("test") { useJUnitPlatform() maxParallelForks = 4 } tasks.named<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>("compileKotlin") { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) } } ``` `tasks.named<T>(name)` returns a `TaskProvider<T>` and only configures the task **if/when it is realized** — this is **configuration avoidance**, which keeps large builds fast by not creating/configuring tasks you never run. ## Registering a new task ```kotlin val docsZip = tasks.register<Zip>("docsZip") { archiveFileName.set("docs.zip") from("build/docs") } ``` `tasks.register<T>(name)` lazily creates the task; `tasks.create<T>(name)` is the **eager** equivalent and is discouraged. ## Configuring all tasks of a type ```kotlin tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" } ``` `withType<T>()` plus `configureEach { }` applies configuration lazily to every current and future task of that type. ## Eager vs lazy — why it matters | API | Lazy? | Notes | |---|---|---| | `tasks.register<T>` / `tasks.named<T>` | yes | preferred; configuration avoidance | | `tasks.create<T>` / `tasks.getByName` | no | realizes immediately; avoid in big builds | | `withType<T>().configureEach` | yes | preferred over `withType<T>().all` | ## Common Kotlin-DSL pitfalls vs Groovy - Use **`tasks.named<Jar>("jar")`** not the Groovy-ish `jar { }` string access — the typed generic gives you the right receiver and lazy semantics. - Set lazy properties with **`.set(...)`** (or the property assignment Gradle enables), not Groovy field assignment. - Provide the **explicit type parameter** so the receiver isn't just `Task` (which lacks the specific members you want).
- Why prefer tasks.named<Test>("test") over tasks.getByName("test")?named is lazy (configuration avoidance) and returns a typed TaskProvider<Test> so you get the correct receiver and autocompletion; getByName eagerly realizes the task and returns a plain Task.
- Why do some properties use .set(...) instead of =?They are lazy Property/Provider types from Gradle's provider API; .set assigns a value (or another provider) to be resolved later, enabling lazy/incremental evaluation.
A lambda-with-receiver block is like handing a contractor the keys to one specific room: inside, every 'the wall', 'the floor' refers to that room — the receiver — without you naming it each time.
saying these in an interview costs you the question
- Using eager tasks.create / getByName in large builds
- Omitting the type parameter so the receiver is just Task
- Not understanding blocks are lambdas with receiver
- Assigning lazy properties with = where .set is required
- Configuring tasks with Groovy-style string property access in Kotlin DSL