What problem does afterEvaluate {} solve, and why is relying on it often a code smell?
answer
- script evaluates top→bottom
- callback after whole script configured
- read user-set values later
- ordering race between plugins
- prefer Provider/Property laziness
basics
~20 safterEvaluate {} defers a block until the whole project script has been configured, so values set later in the script (or by other plugins) are available. It's needed because scripts evaluate top-to-bottom, but overusing it indicates fragile ordering dependencies.
solid answer
~50 sBuild scripts evaluate **top to bottom**, so a plugin or block that reads a configuration value can see only what was set **above** it. `project.afterEvaluate {}` registers a callback that Gradle runs **after the entire project script (and other plugins' configuration) has finished** — letting you read values that the user sets later in their script. Classic use: a plugin that needs the user's extension settings (e.g. `myExt.version`) to wire up tasks, but the user sets them below where the plugin applied. The smell is that `afterEvaluate` couples you to *evaluation order* and creates ordering races between plugins that each `afterEvaluate`; values read there are still **eager** snapshots. The modern, preferred fix is the **lazy Provider/Property API**: a plugin exposes `Property<T>` on its extension and wires tasks to `provider { ... }`/`property` so the value is resolved at execution time regardless of where it was set — no callback, configuration-cache friendly. Reach for `afterEvaluate` only when an API you don't control isn't lazy.
code
kotlin · 9 lines// Smell: depends on evaluation order
afterEvaluate {
tasks.named("jar") { archiveVersion.set(version.toString()) }
}
// Better: lazy provider, no afterEvaluate needed
tasks.named<Jar>("jar") {
archiveVersion.set(provider { project.version.toString() })
}go deeper
Recall that scripts run top-to-bottom and afterEvaluate runs after configuration finishes.
Explain the plugin-reads-user-setting use case and that it depends on evaluation order.
Articulate why it's a smell (ordering races, eager snapshot) and how Provider/Property replaces it.
Set org guidance: ban afterEvaluate in convention plugins, require lazy extension properties, ensure configuration-cache compatibility across shared plugins.
## The ordering problem Gradle configures a project by running its script top to bottom. Consider a plugin applied near the top that wants to use a value the user sets near the bottom: ```kotlin plugins { id("my-publish-plugin") } // plugin applied here, reads version eagerly => sees default myPublish { version = "2.0.0" // set AFTER the plugin already read it } ``` If the plugin read `version` immediately during `apply`, it would see the default, not `2.0.0`. ## What afterEvaluate does `project.afterEvaluate { ... }` registers a callback executed **after the whole project script finishes configuring**. So a plugin can defer reading user settings: ```kotlin class MyPublishPlugin : Plugin<Project> { override fun apply(project: Project) { val ext = project.extensions.create("myPublish", MyExt::class.java) project.afterEvaluate { // now ext.version reflects what the user set anywhere in the script project.tasks.register("publishIt") { /* use ext.version */ } } } } ``` ## Why it is a smell 1. **Ordering races**: if two plugins both use `afterEvaluate`, the order they run in (registration order) becomes load-bearing and brittle. 2. **Eager snapshot**: it captures a value at one moment; if something changes it later, you miss it. 3. **Configuration-cache friction**: callbacks reading mutable project state at configuration time are exactly what lazy APIs avoid. 4. **Hard to reason about**: configuration becomes non-local — behavior depends on the *end state* of the script, not the code you're looking at. ## The modern fix: Provider/Property Expose lazy types on the extension and wire tasks to them: ```kotlin abstract class MyExt { abstract val version: Property<String> } // in apply(): project.tasks.register("publishIt", PublishTask::class.java) { it.version.set(ext.version) // a Provider — resolved at execution time } ``` Now it doesn't matter where the user sets `ext.version.set("2.0.0")`; the value is read **lazily** when the task runs. No `afterEvaluate`, no ordering race, and it's compatible with the configuration cache. Use `afterEvaluate` only as a bridge for third-party APIs that are not yet lazy.
- How do Provider/Property eliminate the need for afterEvaluate?They defer value resolution to execution time. A task wired with `someProperty.set(provider)` reads the value lazily when it runs, so it doesn't matter where in the script the user set it — no callback or ordering dependency.
- What goes wrong when two plugins both use afterEvaluate to configure the same thing?Their callbacks run in registration order, so behavior depends on which plugin was applied first — a brittle, hidden ordering dependency that breaks when apply order changes.
- Is afterEvaluate ever still acceptable?Yes, as a pragmatic bridge when integrating a third-party/legacy API that isn't lazy and forces an eager read; prefer it over hard-coding evaluation order assumptions, but migrate to lazy APIs when possible.
saying these in an interview costs you the question
- Recommending afterEvaluate as the default way to read extension values
- Claiming afterEvaluate values are resolved lazily at execution time
- Not knowing Provider/Property is the preferred alternative