skip to content

How do you obtain ProjectLayout inside a plugin or non-Project class without referencing project, and what convention defaults would you set for output locations?

level: seniorimportance: nice to knowfreq 22%

answer

  1. @Inject ProjectLayout
  2. avoid project.layout (CC-safe)
  3. convention() = overridable default
  4. default under buildDirectory
  5. finalizeValueOnRead / disallowChanges

basics

~10 s

Inject ProjectLayout via @Inject in a task or plugin-instantiated class instead of calling project.layout. Then set conventions like outputFile.convention(layout.buildDirectory.file("...")) so outputs default under build/ but stay overridable.

solid answer

~40 s

`ProjectLayout` is a Gradle-managed service, so the idiomatic way to get it outside `Project` is dependency injection: declare an `@Inject` constructor parameter (or `@Inject` getter) of type `ProjectLayout` on a task, extension, or other Gradle-instantiated type. This avoids capturing `project`, which keeps the code configuration-cache compatible. With the injected `layout` you set **convention defaults** for output properties — `convention()` provides a value used unless the user overrides it. A good pattern: in `Plugin.apply`, create an extension whose `DirectoryProperty`/`RegularFileProperty` defaults to `layout.buildDirectory.dir("<plugin>")`, then have tasks derive children from the extension's base. This makes outputs land under `build/` by default, follow a relocated build dir, and remain user-overridable — all lazily. You can also pair `convention()` with `finalizeValueOnRead()`/`disallowChanges()` when you want to lock values after configuration.

code

kotlin · 7 lines
kotlin
abstract class WriteTask @Inject constructor(
    layout: ProjectLayout
) : DefaultTask() {
    @get:OutputFile abstract val out: RegularFileProperty
    init { out.convention(layout.buildDirectory.file("write/out.txt")) }
    @TaskAction fun run() = out.get().asFile.writeText("x")
}

go deeper

for a junior

Know you can default an output to layout.buildDirectory.file(...) via convention().

for a middle

Use @Inject ProjectLayout and explain convention() as an overridable default.

for a senior

Design extension+task wiring with convention defaults under buildDirectory and value-locking; keep it configuration-cache-safe.

for a principal

Set org-wide conventions for plugin output locations (relocatable, overridable, CC-safe) so caching and build hygiene are uniform.

## Getting ProjectLayout without project Gradle instantiates tasks, extensions, and many helper objects through its object factory, so it can satisfy `@Inject` parameters for known services. `ProjectLayout` is such a service: ```kotlin abstract class WriteTask @Inject constructor( private val layout: ProjectLayout ) : DefaultTask() { @get:OutputFile abstract val out: RegularFileProperty init { out.convention(layout.buildDirectory.file("write/out.txt")) } @TaskAction fun run() = out.get().asFile.writeText("x") } ``` Injecting (rather than `project.layout`) means the task never touches `Project` at execution time — required for the configuration cache. The same `@Inject` works for `ObjectFactory`, `ProviderFactory`, etc., but here we focus on `ProjectLayout`. ## Convention defaults for output locations `Property.convention(value)` sets a *default* that applies only if no explicit value is set. This is the right tool for plugin defaults: ```kotlin abstract class ReportExtension { abstract val outputDir: DirectoryProperty } class ReportPlugin : Plugin<Project> { override fun apply(project: Project) { val layout = project.layout val ext = project.extensions.create("report", ReportExtension::class.java) // default under build/, still overridable ext.outputDir.convention(layout.buildDirectory.dir("reports")) project.tasks.register("report") { val target = ext.outputDir.file("report.html") // derives from base it.outputs.file(target) it.doLast { target.get().asFile.writeText("<html/>") } } } } ``` Key properties of this design: - **Default-but-overridable:** users can do `report { outputDir.set(layout.projectDirectory.dir("site")) }` and every derived child follows. - **Relocatable:** because the default is a `layout.buildDirectory` Provider, relocating the build dir moves the reports. - **Lazy:** nothing resolves until execution. ## Locking values When you want to prevent later mutation: - `property.finalizeValueOnRead()` — value is computed and frozen on first read. - `property.disallowChanges()` — any further `set()` throws. - `property.finalizeValue()` — explicitly freeze now. These guard plugin invariants without sacrificing the convention default. ## Why this matters at scale Standardizing on injected `ProjectLayout` + `convention(layout.buildDirectory...)` gives a plugin suite consistent, relocatable, configuration-cache-safe output locations — the foundation for reliable caching and clean `build/` semantics across an org.

  • Why inject ProjectLayout instead of using project.layout in a task action?
    Injecting avoids referencing Project at execution time, which the configuration cache forbids. project.layout inside an action would fail.
  • What's the difference between convention() and set() for a default output location?
    convention() is a fallback applied only when no value is set, so users can still override; set() imposes a value that user set() calls would override or conflict with, depending on order.
  • How do you stop users from changing a property after the plugin configures it?
    Call finalizeValueOnRead(), disallowChanges(), or finalizeValue() to lock the property.

saying these in an interview costs you the question

  • Calling project.layout inside @TaskAction/doLast instead of injecting it.
  • Using set() where convention() is meant, making defaults non-overridable or order-dependent.
  • Hardcoding absolute output paths instead of deriving from layout.buildDirectory.

context