What does the InputChanges API give a task action that ordinary up-to-date checking does not, and when does a task receive it?
answer
- @Incremental input + InputChanges param
- getFileChanges -> ADDED/MODIFIED/REMOVED
- isIncremental true = precise delta
- false = everything reported ADDED
- you must delete outputs for REMOVED
basics
~10 sUp-to-date checking only decides skip-or-rerun for the whole task. InputChanges tells the action exactly which input files were added, modified, or removed, so it can reprocess only those instead of everything.
solid answer
~40 sGradle's normal incremental-build feature is coarse: it compares input/output snapshots and decides to either skip a task entirely (UP-TO-DATE) or run it from scratch. A task with an `@Incremental`-annotated file input can instead accept an `InputChanges` parameter in its action. When inputs change in a way Gradle can track, `InputChanges.isIncremental` is `true` and `getFileChanges(input)` yields only the changed files with a `ChangeType` of ADDED, MODIFIED, or REMOVED — so the action processes just those. When Gradle can't determine a clean delta (first run, an untracked-output or non-file input changed, or output was tampered with), `isIncremental` is `false` and `getFileChanges` returns *every* file as ADDED, so the action must handle a full rebuild. Net effect: finer-grained work avoidance *inside* a single task execution.
code
kotlin · 11 lines@TaskAction
fun run(changes: InputChanges) {
if (!changes.isIncremental) outputDir.get().asFile.deleteRecursively()
changes.getFileChanges(inputDir).forEach { c ->
val out = outputDir.file(c.normalizedPath).get().asFile
when (c.changeType) {
ChangeType.REMOVED -> out.delete()
else -> process(c.file, out)
}
}
}go deeper
Know that it reports which files changed (added/modified/removed) so the task does less work.
Explain the two modes (isIncremental true vs false) and that false reports all files as ADDED.
Tie it to up-to-date checking granularity, the requirement to clean REMOVED outputs, and why non-incremental fallbacks occur.
Frame when investing in incremental task actions pays off (large file sets, expensive per-file work) vs. relying on the cache, and the maintenance risk of the dual-path action.
## The problem InputChanges solves Gradle's everyday incrementality works at the **task** granularity. Before running a task, Gradle snapshots its declared inputs (files + their normalized content hashes, plus input properties) and its outputs. If the snapshot matches the previous successful run, the task is marked `UP-TO-DATE` and skipped. Otherwise the whole task action runs again. There is no middle ground at this level: change one source file in a directory of 10,000 and the task reruns over all 10,000. **Incremental tasks with `InputChanges`** add a finer layer: the task action itself receives a description of *which* files changed since the last run and can do proportional work. ## Wiring it up (consumer side) The input you want tracked at file granularity is annotated `@Incremental` (alongside `@InputFiles`/`@InputDirectory` and a normalization annotation like `@PathSensitive`). The task action then declares a parameter of type `org.gradle.work.InputChanges`: - `inputChanges.isIncremental` — `true` when Gradle could compute a precise delta; `false` when it could not. - `inputChanges.getFileChanges(fileProperty)` — returns an `Iterable<FileChange>`; each `FileChange` exposes `getFile()`, `getChangeType()` (`ChangeType.ADDED` / `MODIFIED` / `REMOVED`), `getNormalizedPath()`, and `getFileType()`. ## The two execution modes 1. **Incremental run** (`isIncremental == true`): `getFileChanges` lists *only* the files that actually changed. Your action processes ADDED/MODIFIED files and deletes the outputs corresponding to REMOVED files. This is the fast path. 2. **Non-incremental run** (`isIncremental == false`): `getFileChanges` reports *every* input file as `ADDED`. Gradle falls back to this when it cannot trust a partial delta — the very first execution, a change to a non-`@Incremental` input or an input property, an output directory that was modified outside Gradle, or a change Gradle classifies as not incrementally trackable. Your action must be written to also rebuild everything in this case (usually: clear the output directory first, then process all 'changes'). ## Why REMOVED matters Because an incremental action only touches changed inputs, it is solely responsible for cleaning up stale outputs. If `B.txt` is deleted, Gradle won't re-run the whole task — it just tells you `B.txt` is `REMOVED`, and your code must delete the derived `B.out`. Forgetting this leaves orphaned outputs. ```kotlin abstract class TransformFiles : DefaultTask() { @get:Incremental @get:PathSensitive(PathSensitivity.RELATIVE) @get:InputDirectory abstract val sources: DirectoryProperty @get:OutputDirectory abstract val outputDir: DirectoryProperty @TaskAction fun execute(changes: InputChanges) { if (!changes.isIncremental) { project.delete(outputDir) // full rebuild path } changes.getFileChanges(sources).forEach { change -> val target = outputDir.file(change.normalizedPath).get().asFile when (change.changeType) { ChangeType.REMOVED -> target.delete() else -> if (change.fileType != FileType.DIRECTORY) { target.parentFile.mkdirs() target.writeText(change.file.readText().uppercase()) } } } } } ``` ## Relationship to the build cache InputChanges is purely a *local* optimization: it relies on the previous outputs still being present in the project. A cache hit (or a clean checkout) replaces outputs wholesale and is unrelated; in fact after a cache hit there is no prior local state, so the next change triggers a non-incremental run.
- If isIncremental is false, what does getFileChanges return?Every current input file, each reported with ChangeType.ADDED — Gradle's signal that you must do a full rebuild.
- Does using InputChanges change whether the task is skipped as UP-TO-DATE?No. The up-to-date decision is unchanged; InputChanges only refines what happens once Gradle has already decided the task must run.
Up-to-date check is a smoke alarm: it only says 'something burned, redo the room'. InputChanges is a checklist of exactly which items got scorched, so you replace only those.
saying these in an interview costs you the question
- Claiming InputChanges lets Gradle skip the task — it operates only after Gradle decides to run it.
- Saying you can ignore the non-incremental (isIncremental == false) branch — that breaks first runs and after-cache-hit runs.