skip to content

A plugin reads myExt.message.get() inside its apply() method and the user's configured value is always missing. What's wrong and how do you fix it while still creating the extension correctly?

level: seniorimportance: must knowfreq 45%

answer

  1. apply() runs before user's config block
  2. .get() eager = reads too early
  3. wire provider into task property
  4. use map/flatMap to stay lazy
  5. create eager, consume lazy

basics

~20 s

The extension is created in apply(), but the user's myExt { } block runs later as the script evaluates. Reading .get() in apply() observes the value before configuration. Fix it by consuming the Property lazily — pass the provider through instead of calling get() eagerly.

solid answer

~40 s

`extensions.create` runs when the plugin is **applied**, but the user's `myExt { message.set(...) }` block runs **later**, as the build script is evaluated. Calling `myExt.message.get()` inside `apply()` therefore reads the property **before** the user has set it, yielding the convention/default or an empty value. The extension creation itself is fine — the bug is **eager consumption**. The fix is to keep everything lazy: don't call `.get()` at apply time; instead pass the `Property`/`Provider` itself wherever you need the value. For example, when registering a task, do `task.outputMessage.set(myExt.message)` (provider-to-property wiring) so the value is only resolved when the task executes. If you must derive something, use `myExt.message.map { ... }` to stay lazy. The golden rule: create the extension eagerly, but consume its values through providers, never via `.get()` during configuration.

code

kotlin · 13 lines
kotlin
abstract class GreetTask : DefaultTask() {
    @get:Input abstract val message: Property<String>
    @TaskAction fun run() = println(message.get())  // resolved at execution
}

class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val ext = project.extensions.create("greeting", GreetingExtension::class.java)
        project.tasks.register("greet", GreetTask::class.java) { task ->
            task.message.set(ext.message)   // lazy wiring, no .get() at apply time
        }
    }
}

go deeper

for a junior

Recognize the value is read too early and that user configuration happens after the plugin is applied.

for a middle

Explain the apply-vs-configuration timing and that you should pass the Property rather than calling .get() in apply().

for a senior

Articulate provider wiring (set(provider), map/flatMap), why it's preferred over afterEvaluate, and the link to configuration avoidance.

for a principal

Connect lazy consumption to configuration-cache correctness and build-performance governance; set authoring rules that ban eager .get() in plugin configuration paths.

## The lifecycle mismatch Gradle's build has phases. A plugin's `apply(project)` runs during **initialization of that plugin** — very early. The user's configuration block: ```kotlin myExt { message.set("Hello") } ``` runs during the **configuration phase**, when Gradle evaluates the build script body. Crucially, `apply()` typically finishes *before* the script body that configures the extension executes (or at least before the user's block runs). So any value you read eagerly in `apply()` reflects the pre-configuration state. ## Why `.get()` is the trap `Property<T>.get()` **realizes** the value immediately. Call it in `apply()` and you snapshot the default/convention, missing whatever the user sets afterward. ```kotlin // BUG: eager read at apply time override fun apply(project: Project) { val ext = project.extensions.create("myExt", MyExtension::class.java) val msg = ext.message.get() // too early! project.tasks.register("greet") { it.doLast { println(msg) } } } ``` ## The lazy fix Keep the `Provider` flowing and only resolve at execution: ```kotlin override fun apply(project: Project) { val ext = project.extensions.create("myExt", MyExtension::class.java) project.tasks.register("greet", GreetTask::class.java) { t -> t.message.set(ext.message) // provider wiring, no get() } } ``` Now `ext.message` is connected to the task's own `Property`; the value is read only when the task runs, by which time the user's block has executed. ## Deriving values lazily If you need a transformation, use `map`/`flatMap` which return providers: ```kotlin val shout = ext.message.map { it.uppercase() } ``` `shout.get()` is not called until something downstream needs it. ## Why this also helps performance / configuration avoidance Lazy consumption pairs with `tasks.register` (configuration avoidance) so tasks aren't configured unless needed, and avoids forcing values during configuration. Eager `.get()` can also break up-to-date checks and configuration-cache compatibility. ## Summary rule Create the extension eagerly (you need the object to exist so the DSL block resolves), but **consume** its properties lazily — pass providers, use `map`, and let `.get()` happen at execution time inside the task.

  • Why doesn't moving the create() call later in apply() fix the problem?
    The user's myExt { } block still runs during configuration, after apply() returns. The issue isn't where create is called but that .get() resolves the value before configuration runs; you must defer resolution, not reorder creation.
  • How does lazy consumption interact with the configuration cache?
    Eagerly calling .get() during configuration can capture state that the configuration cache can't model or that becomes stale. Wiring providers lets values be resolved at execution, which is configuration-cache friendly.
  • If you genuinely need to react to the configured value once, what's a lazy-safe option?
    Use the value through a provider (e.g. task input), or use project.afterEvaluate sparingly to read after configuration — though provider wiring is preferred over afterEvaluate.

Calling .get() in apply() is like reading a form the instant you hand it out, before anyone has filled it in. Passing the provider is handing the still-blank form forward and reading it only when it's finally needed.

saying these in an interview costs you the question

  • Suggesting the fix is to construct the extension differently — creation is correct; consumption is the bug.
  • Recommending sprinkling afterEvaluate everywhere instead of provider wiring.
  • Believing .get() at apply time is safe because the extension object already exists — the object exists but isn't configured yet.

context