skip to content

When driving a Gradle build through the Tooling API, how do you receive progress updates so an IDE can show build feedback?

level: juniorimportance: must knowfreq 45%

answer

  1. addProgressListener on the launcher
  2. ProgressListener.statusChanged(ProgressEvent)
  3. OperationType to scope events
  4. StartEvent vs FinishEvent
  5. events streamed during the build

basics

~10 s

Register a ProgressListener on the build launcher via addProgressListener(...). Gradle then streams ProgressEvent objects to your callback as the build runs, which the IDE turns into progress UI.

solid answer

~40 s

The Tooling API is the official programmatic entry point that lets an IDE or tool drive a Gradle build out-of-process. To get live feedback, you build a `BuildLauncher` (or `ModelBuilder`/`TestLauncher`) from a `ProjectConnection` and call `addProgressListener(ProgressListener, OperationType...)`. Gradle then invokes your listener's `statusChanged(ProgressEvent)` on its own thread for each event in the build. You typically subscribe to specific `OperationType`s such as `TASK`, `TEST`, or `PROJECT_CONFIGURATION` so you only get the events you render. Each `ProgressEvent` carries a descriptor and a timestamp; the concrete subtypes `StartEvent` and `FinishEvent` mark when an operation begins and ends, letting the IDE draw progress bars, tree nodes, or test results. The listener runs while the build is executing, so updates arrive incrementally rather than all at once at the end.

code

kotlin · 14 lines
kotlin
GradleConnector.newConnector().forProjectDirectory(projectDir).connect().use { conn ->
    conn.newBuild()
        .forTasks("build")
        .addProgressListener(
            ProgressListener { event ->
                when (event) {
                    is StartEvent  -> println("START  ${'$'}{event.descriptor.displayName}")
                    is FinishEvent -> println("FINISH ${'$'}{event.descriptor.displayName}")
                }
            },
            OperationType.TASK, OperationType.TEST
        )
        .run()
}

go deeper

for a junior

Know that you register a ProgressListener with addProgressListener and that Gradle streams events to it while the build runs.

for a middle

Explain OperationType scoping and the StartEvent/FinishEvent pair, and name the launcher types (BuildLauncher/ModelBuilder/TestLauncher).

for a senior

Discuss the typed events package vs the legacy string ProgressListener, OperationResult inspection, and threading concerns.

for a principal

Frame how IDE tooling consumes this stream to build responsive UIs, and the contract stability/version-compatibility considerations across Gradle versions.

## What the Tooling API is The **Gradle Tooling API** is a small client library an IDE (IntelliJ IDEA, Eclipse Buildship) or custom tool uses to launch and inspect Gradle builds *programmatically*, in a separate Gradle daemon process. You obtain a `ProjectConnection`, then create a launcher and configure it before running. ## The progress-listener mechanism To show a user what the build is doing in real time, you attach a listener: ```kotlin launcher.addProgressListener( ProgressListener { event -> render(event) }, OperationType.TASK, OperationType.TEST ) ``` The modern overload takes an `org.gradle.tooling.events.ProgressListener` plus a varargs/Set of `OperationType` values. Gradle then calls `statusChanged(ProgressEvent)` for every matching event as the build runs. ## The event hierarchy - **`ProgressEvent`** is the base type. Every event has a `getEventTime()` (epoch millis) and a `getDescriptor()` describing the operation. - **`StartEvent`** fires when an operation begins. - **`FinishEvent`** fires when it ends and carries an `OperationResult` (success / failure / skipped). - More specific subtypes exist per operation type, e.g. `TaskStartEvent`/`TaskFinishEvent`, `TestStartEvent`/`TestFinishEvent`, each exposing a typed descriptor (`TaskOperationDescriptor`, `JvmTestOperationDescriptor`, ...). ## OperationType — scoping what you receive `OperationType` is an enum that lets you subscribe only to the categories you care about: `TASK`, `TEST`, `PROJECT_CONFIGURATION`, `WORK_ITEM`, `TRANSFORM`, `FILE_DOWNLOAD`, and more. Subscribing narrowly keeps the event stream small and your rendering code simple. ## Why it matters This is exactly how IDEs draw the live build tree, per-task progress, and test result panes. Because the listener is invoked incrementally during execution (not buffered to the end), the UI stays responsive. There is also a legacy `org.gradle.tooling.ProgressListener` whose `statusChanged(ProgressEvent)` yields only coarse, string-based status — the typed `events` package is preferred for anything structured. ## Threading caveat Gradle invokes the listener from its own thread, so any UI mutation must be marshalled back onto the IDE's UI thread, and the callback should be fast and non-blocking.

  • What is the difference between StartEvent and FinishEvent?
    StartEvent fires when an operation begins and only carries the descriptor; FinishEvent fires when it completes and additionally carries an OperationResult indicating success, failure, or skipped, plus end time.
  • Why pass OperationType arguments instead of listening to everything?
    OperationType scopes the subscription so you only receive events for the categories you render (e.g. just TASK and TEST), reducing event volume and keeping the listener logic simple.

saying these in an interview costs you the question

  • Claiming progress events only arrive after the build finishes — they stream incrementally during execution.
  • Confusing the Tooling API ProgressListener with a Gradle plugin/task callback inside the build.

context