Some teams write afterEvaluate { if (plugins.hasPlugin("java")) { ... } } to configure things conditionally. Why is reacting with withPlugin/withType generally better?
answer
- afterEvaluate = end of config
- fragile inter-block ordering
- withPlugin fires at apply time
- lazy named/Provider beats waiting
- afterEvaluate = last resort
basics
~20 safterEvaluate delays all your config to the end of configuration and runs even if nothing changed; withPlugin runs exactly when the plugin appears, is lazy and ordering-safe, and avoids the timing pitfalls and lifecycle fragility of afterEvaluate.
solid answer
~50 s`afterEvaluate { if (plugins.hasPlugin("java")) ... }` is a blunt instrument: it postpones your whole configuration block until the project is fully evaluated, then snapshots state. That creates problems — other `afterEvaluate` blocks may run before or after yours (order is fragile), values you read may already be 'frozen' for consumers, and it's easy to introduce hard-to-debug timing bugs. `pluginManager.withPlugin("java") { }` instead fires precisely at the moment the plugin is applied (or immediately if already applied), so configuration happens at the natural point and composes cleanly. It also expresses intent better: 'when java is applied, do this' rather than 'at the very end, check whether java happened'. Reacting is the idiomatic, lazy, order-independent approach; `afterEvaluate` is a last resort for genuine cross-project or post-DSL-finalization needs. With lazy `Provider`/`Property` wiring you rarely need `afterEvaluate` at all.
code
kotlin · 9 lines// Fragile: delayed, order-dependent
afterEvaluate {
if (plugins.hasPlugin("java")) tasks.named<Test>("test") { useJUnitPlatform() }
}
// Better: reacts at the right moment, lazy task wiring
pluginManager.withPlugin("java") {
tasks.named<Test>("test") { useJUnitPlatform() }
}go deeper
Know that withPlugin is preferred and afterEvaluate is a heavier, later-running mechanism.
Explain the timing difference and at least one fragility of afterEvaluate.
Contrast lifecycle timing, ordering pitfalls, and how lazy Provider wiring removes most afterEvaluate use.
Set org-wide guidance: convention plugins react + wire lazily; afterEvaluate requires justification in review to keep build logic maintainable and config-cache friendly.
## What afterEvaluate actually does Gradle configures each project in a phase. `afterEvaluate { }` schedules a block to run **after the project's build script has finished evaluating** but still within the configuration phase. People reach for it to do conditional configuration: 'by the end, if the java plugin was applied, set X'. ```kotlin // Common but fragile afterEvaluate { if (plugins.hasPlugin("java")) { tasks.named<Test>("test") { useJUnitPlatform() } } } ``` ## Why this is fragile 1. **Ordering between afterEvaluate blocks.** Multiple plugins/scripts can register `afterEvaluate` callbacks. They run in registration order, which is hard to predict and reason about. Two plugins both using `afterEvaluate` can produce order-dependent results. 2. **Too-late configuration.** Some things are read *during* evaluation by other code; if you only set them in `afterEvaluate`, consumers may have already observed the old value. 3. **All-or-nothing delay.** Everything in the block is delayed to the end, even parts that didn't need to be, hurting clarity and sometimes correctness. 4. **Doesn't express intent.** 'At the end, check a flag' is weaker than 'react when the capability appears'. ## Why reacting is better `withPlugin`/`withType` runs the action **at the moment the plugin is applied** — earlier than `afterEvaluate`, at exactly the right time, and only if the plugin is actually present. Combined with **lazy configuration** (`tasks.named`, `Provider`, `Property`), the actual values are resolved even later, at execution time, so you don't need to 'wait until the end' to read final values. ```kotlin pluginManager.withPlugin("java") { // runs right when java is applied; tasks.named is lazy so final values still resolve later tasks.named<Test>("test") { useJUnitPlatform() } } ``` ## When afterEvaluate is still legitimate - Reading a value a user sets in **their own** build script that genuinely can only be known after their script finishes (and that you can't model as a lazy `Property`). - Certain cross-project wiring where you must wait for another project's evaluation. Even these are increasingly replaced by lazy `Provider` chains and `project.provider { }`. The rule of thumb: **react to plugins with withPlugin/withType, configure lazily with named/Provider, and treat afterEvaluate as a smell to justify, not a default.**
- When is afterEvaluate genuinely justified?When you must read a value only knowable after the user's script finishes and can't be modelled as a lazy Property, or for some cross-project wiring that depends on another project's evaluation.
- How does lazy configuration reduce the need for afterEvaluate?Provider/Property and tasks.named defer value resolution to execution time, so you don't need to wait until the end of configuration to read final values.
saying these in an interview costs you the question
- Treating afterEvaluate as the default tool for any conditional configuration.
- Assuming afterEvaluate blocks have a guaranteed, predictable order across plugins.