What are the different ways to specify the argument to dependsOn, and why is passing a TaskProvider preferred over a task name string?
answer
- String / Task / TaskProvider / Provider / Collection / closure
- tasks.named = lazy provider
- getByName = eager realize
- configuration avoidance
- :path:task cross-project string
basics
~20 sYou can pass a task name string, a Task or TaskProvider reference, a Provider, a Collection, or a closure. A TaskProvider is preferred because it is lazy — it doesn't force the other task to be created/configured eagerly.
solid answer
~40 s`dependsOn` accepts many forms: a `String` name (`dependsOn("test")`), a `Task`, a `TaskProvider` from `tasks.named(...)`/`tasks.register(...)`, a `Provider`, a `Collection`, or a `Callable`/closure that resolves lazily at graph-build time. The string and `Task` forms are convenient but the `TaskProvider` form is preferred under Gradle's **configuration avoidance** model: `tasks.named("x")` returns a provider without realizing task `x`, so the dependency target is only configured if it actually ends up in the graph. Passing a raw `Task` (e.g. `tasks.getByName("x")`) forces that task to be created and configured immediately during the configuration phase, hurting build performance. A closure form like `dependsOn { computeTasks() }` defers resolution entirely until Gradle builds the execution graph.
code
kotlin · 7 lines// Preferred: lazy, type-safe
val compile = tasks.named("compileJava")
tasks.register("verify") {
dependsOn(compile) // TaskProvider — no eager realization
dependsOn("test") // String — also lazy but untyped
dependsOn(":lib:jar") // cross-project by path
}go deeper
List the common forms (name string, task reference) and that multiple can be passed.
Explain configuration avoidance and why TaskProvider/named beats getByName for performance.
Discuss lazy closures/providers for dynamic graphs and the cross-project path-string form, plus trade-offs.
Tie configuration-avoidance discipline to build-time SLAs across a large module graph and set conventions for the team.
## The accepted argument types `Task.dependsOn(Object...)` is deliberately polymorphic. Gradle resolves each argument when it builds the task graph. Accepted forms include: - **`String` (task name)** — `dependsOn("compileJava")`. Resolved by name within the project (or `:path:task` across projects). - **`Task`** — a realized task instance. - **`TaskProvider`** — returned by `tasks.named(...)` and `tasks.register(...)`. **Lazy.** - **`Provider<Task>` / `Provider<...>`** — resolved on demand. - **`Collection` / array** — each element resolved as above. - **`Callable` / closure** — `dependsOn { ... }`; the closure is invoked when the graph is built and may return any of the above. ## Why TaskProvider wins: configuration avoidance Gradle's **configuration-avoidance API** (`register`/`named`) exists so tasks that aren't needed are never configured. A `TaskProvider` is a *handle* — referencing it does **not** realize the underlying task. If the dependent task never enters the graph, the target is never configured. Contrast with eager access: ```kotlin // EAGER — forces task 'foo' to be configured right now dependsOn(tasks.getByName("foo")) // LAZY — preferred; 'foo' configured only if actually needed dependsOn(tasks.named("foo")) ``` In large multi-module builds, eager realization cascades and inflates configuration time. Strings are also lazy (resolved by name later) but they lose type safety and IDE navigation, and a typo surfaces only at graph time. `TaskProvider` gives both laziness *and* compile-time/IDE awareness. ## Closures for dynamic dependencies ```kotlin tasks.register("aggregate") { dependsOn(provider { tasks.matching { it.name.startsWith("report") } }) } ``` The closure/provider is evaluated lazily, so it can depend on configuration decided later. Use sparingly — it is harder to reason about than explicit edges. ## Cross-project form A string can carry a project path: `dependsOn(":lib:build")` depends on the `build` task of subproject `:lib`. Prefer wiring through providers/project references in modern builds, but the path-string form is common in older scripts.
- Why does dependsOn(tasks.getByName("x")) hurt configuration performance?getByName realizes and configures task x immediately during the configuration phase, even if x might never be needed. tasks.named returns a lazy provider that defers configuration until the task is actually in the graph.
- How do you depend on a task in another subproject via a string?Use a fully-qualified path: dependsOn(":otherModule:taskName"). The leading colon roots it at the build, and the path resolves that project's task.
saying these in an interview costs you the question
- Claiming string names are always bad — they are lazy too; the real downside is lost type safety and late typo detection.
- Saying tasks.named realizes the task — it returns a provider and defers realization.