In a build.gradle.kts script, what is the implicit receiver, and why can you call things like dependencies {} or repositories {} without any qualifier?
answer
- lambda with receiver: Project.() -> Unit
- script body receiver = Project (KotlinBuildScript)
- nested blocks = own receiver (DependencyHandler, RepositoryHandler)
- receiver stack, nearest wins
- drives autocomplete + type-safe accessors
basics
~10 sThe script body runs with an implicit receiver of type Project (a KotlinBuildScript). Calls like dependencies {} or repositories {} are member functions on that Project receiver, so you can call them unqualified.
solid answer
~40 sA `build.gradle.kts` file is compiled as a Kotlin script whose body executes against an **implicit receiver** — conceptually the `Project`, exposed through the `KotlinBuildScript` base class. That is why `dependencies { }`, `repositories { }`, `tasks { }`, and `project`, `version`, `group` are all available unqualified: they are members/extensions on the receiver, just as inside a Kotlin lambda with receiver you can call the receiver's members directly. Each nested block is itself a lambda with its own receiver: inside `dependencies { }` the receiver is a `DependencyHandler`, inside `repositories { }` it's a `RepositoryHandler`. This receiver chain, combined with type-safe accessors generated from applied plugins, is what gives the Kotlin DSL its precise IDE autocompletion. Understanding it explains scoping surprises — e.g. `this` inside a block refers to that block's handler, not the Project.
code
kotlin · 11 lines// Whole body runs against Project as implicit receiver
group = "com.example"
version = "1.0.0"
repositories { // receiver here = RepositoryHandler
mavenCentral()
}
dependencies { // receiver here = DependencyHandler
implementation("com.google.guava:guava:33.0.0-jre")
}go deeper
It's enough to know you can call dependencies/repositories without a prefix because of an implicit object.
Name the Project receiver and recognise that nested blocks have their own receivers.
Explain lambdas-with-receiver, the receiver stack, and how it underpins autocompletion and type-safe accessors.
Relate the receiver model to designing custom DSL extensions/convention plugins that integrate cleanly into the script's receiver chain.
## Lambdas with receiver — the Kotlin foundation Kotlin supports **function types with a receiver**, e.g. `Project.() -> Unit`. When you call such a lambda, the receiver object becomes the implicit `this` inside it, so you can call the receiver's members without naming it. This is the language feature the whole Gradle Kotlin DSL is built on. ## The script's outer receiver Gradle compiles `build.gradle.kts` into a subclass of `KotlinBuildScript`, which delegates to the project. Effectively the **entire script body runs with `Project` as the implicit receiver**. That is why these all work at the top level with no qualifier: ```kotlin group = "com.example" // Project.setGroup version = "1.0.0" repositories { mavenCentral() } // Project.repositories(RepositoryHandler.() -> Unit) dependencies { // Project.dependencies(DependencyHandler.() -> Unit) implementation("a:b:1.0") } tasks { /* ... */ } // Project.tasks(...) ``` `repositories`, `dependencies`, `tasks`, `group`, `version`, `project` are all members or extensions reachable on the `Project` receiver. ## Nested receivers Each configuration block introduces its **own** receiver: - inside `repositories { }` → `this` is a `RepositoryHandler` (so `mavenCentral()` resolves). - inside `dependencies { }` → `this` is a `DependencyHandler` (so `implementation(...)` resolves). So there is a *stack* of receivers. Inside `dependencies { }`, an unqualified call first looks at the `DependencyHandler`, then outward toward the `Project`. This occasionally causes ambiguity, which Kotlin resolves by nearest receiver; you can disambiguate with a labeled `this@`. ## Why it matters 1. **Autocompletion** — because the receiver type is known statically, the IDE lists exactly the members available in each block. 2. **Type-safe accessors** — once a plugin is applied, Gradle generates extension accessors on the relevant receiver (e.g. the `application { }` block), all statically typed. 3. **Debugging scope confusion** — knowing that `this` changes per block explains why a property visible at top level may be shadowed or unreachable inside a nested block. ## Mental model Think of the whole script as one big `project.apply { ... }` where `project` is the receiver, and every `{ }` you open swaps in a more specific receiver for that block.
- What is the receiver type inside the dependencies {} block?A DependencyHandler. That is why methods like implementation(...) and project(...) resolve there without qualification.
- How does the implicit-receiver design enable IDE autocompletion?Because each block has a known static receiver type, the IDE can list exactly that type's members; combined with generated type-safe accessors from applied plugins, completion is precise.
- If a name is ambiguous between the block receiver and the Project, how do you disambiguate?Use a labeled this, e.g. this@Project or this@build, to target the outer receiver explicitly.
saying these in an interview costs you the question
- Claiming the whole script is dynamic/untyped — it is statically compiled with typed receivers.
- Assuming this inside dependencies {} is the Project; it is the DependencyHandler.