skip to content

Inside a Plugin<Project>.apply method, how do you register a task and an extension, and how do you connect them?

level: middleimportance: must knowfreq 65%

answer

  1. extensions.create(name, type) → DSL block
  2. tasks.register → lazy TaskProvider
  3. wire provider not .get()
  4. managed abstract Property fields
  5. config ordering solved by laziness

basics

~10 s

Use project.extensions.create("name", Ext::class.java) to add a config block and project.tasks.register("task", Task::class.java) to register a task. Connect them by setting the task's Property inputs from the extension's Provider values.

solid answer

~40 s

Inside `apply`, create the extension with `project.extensions.create("greeting", GreetingExtension::class.java)` — this returns the instance and adds a `greeting { }` DSL block. Register tasks lazily with `project.tasks.register("hello", HelloTask::class.java) { ... }`. The key is **lazy wiring**: the extension holds `Property<String>`/`ListProperty` fields, and you set the task's input `Property` from the extension's `Provider` (e.g. `task.message.set(ext.message)`). Because both are lazy, the user can configure `greeting { message = ... }` *after* the plugin's `apply` runs, and the value is only read at task execution. This sidesteps configuration ordering bugs you'd hit with plain mutable fields. Use `extensions.create` (not manual instantiation) so Gradle decorates the type and injects `ObjectFactory` for `objects.property(...)`.

code

kotlin · 15 lines
kotlin
abstract class GreetingExtension {
    abstract val message: Property<String>
}

abstract class HelloTask : DefaultTask() {
    @get:Input abstract val message: Property<String>
    @TaskAction fun run() = println(message.get())
}

class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val ext = project.extensions.create("greeting", GreetingExtension::class.java)
        project.tasks.register("hello", HelloTask::class.java) { it.message.set(ext.message) }
    }
}

go deeper

for a junior

Know the two calls: extensions.create for config block, tasks.register for the task.

for a middle

Explain lazy provider wiring (set provider not value) and managed abstract Property extensions.

for a senior

Reason about configuration-ordering correctness, decoration, and why eager .get() is a bug; default extension values via convention().

for a principal

Standardize extension/task wiring patterns so teams avoid ordering bugs and keep configuration-time fast at scale.

## Extensions: the user-facing DSL An **extension** is a plain object Gradle attaches to the project to give users a configuration block. You add it with: ```kotlin val ext = project.extensions.create("greeting", GreetingExtension::class.java) ``` The first argument is the **DSL name** (`greeting { }` becomes available in build scripts), the second is the type Gradle instantiates and *decorates*. Decoration matters: Gradle subclasses the type so it can inject services and support managed properties. Define the extension with lazy types: ```kotlin abstract class GreetingExtension { abstract val message: Property<String> } ``` Abstract `Property`/`ListProperty`/`MapProperty` getters are **managed properties** — Gradle implements them, no constructor needed. Alternatively initialize via injected `ObjectFactory`: `objects.property(String::class.java)`. ## Tasks: lazy registration Register tasks with the **lazy** API so they aren't realized unless needed: ```kotlin val hello = project.tasks.register("hello", HelloTask::class.java) { task -> task.message.set(ext.message) } ``` `register` returns a `TaskProvider<HelloTask>`; the configuration lambda runs only when the task is realized. Compare with `tasks.create` which eagerly instantiates and configures every build — avoid it for performance (configuration avoidance). ## Connecting extension → task (the important part) Both the extension field and the task input are lazy `Property` values backed by `Provider`. You wire them by **provider, not value**: ```kotlin task.message.set(ext.message) // wires the provider, value read later ``` Not `task.message.set(ext.message.get())` — calling `.get()` eagerly snapshots the value during `apply`, *before* the user configures `greeting { }`, capturing a stale/default value. By passing the provider, the actual string is resolved only when the task input is queried (execution time), so user configuration applied after `apply` is honored. This decoupling is the whole point of the Provider API and the reason plugins don't suffer from configuration-ordering bugs. ## Putting it together ```kotlin class GreetingPlugin : Plugin<Project> { override fun apply(project: Project) { val ext = project.extensions.create("greeting", GreetingExtension::class.java) project.tasks.register("hello", HelloTask::class.java) { it.message.set(ext.message) } } } ``` Users then write `greeting { message = "Hi" }` and run `hello` — the task reads the configured value lazily.

  • Why set the task input with ext.message instead of ext.message.get()?
    ext.message passes the Provider, deferring value resolution until execution, so user config applied after apply() is honored. .get() reads eagerly during apply, before greeting{} is configured, capturing a stale default.
  • Why use an abstract class with abstract Property getters for the extension?
    They become Gradle-managed properties: Gradle implements the getters and supplies the ObjectFactory-backed Property instances, so you don't write boilerplate or a constructor.
  • What does extensions.create return and why use it over new MyExtension()?
    It returns the decorated instance and registers the DSL block. Direct instantiation skips decoration/service injection and won't expose the configuration block.

saying these in an interview costs you the question

  • Wiring with ext.message.get() inside apply, snapshotting before user configuration.
  • Instantiating the extension manually instead of via extensions.create.
  • Using tasks.create eagerly when register is appropriate.

context