Better Not Depend On Tasks - That's the Old Timey Way
In Gradle it is very common to have one task write to a file, and then that file is read by another task to do more work. A couple of years back these tasks would have looked something like:
open class OldWriter : DefaultTask() {
@get:Input
val list: MutableList<String> = mutableListOf()
@get:OutputFile
lateinit var outputFile: File
@TaskAction
fun write() = outputFile.writeText(list.joinToString("\n"))
}
open class OldReader : DefaultTask() {
@get:InputFile
lateinit var inputFile: File
@get:OutputFile
lateinit var outputFile: File
@TaskAction
fun writeCount() = outputFile.writeText(
"Line count: ${inputFile.readLines().count()}"
)
}
We would then set these tasks up using something like:
val oldWriterFile = project.buildDir.resolve("oldWriter.txt")
val oldWriterTask = tasks.register(
"oldWriter",
OldWriter::class
) {
list.addAll(listOf("apple", "orange", "banana"))
outputFile = oldWriterFile
}
tasks.register("oldReader", OldReader::class) {
inputFile = oldWriterFile
dependsOn(oldWriterTask)
outputFile = project.buildDir.resolve("oldReader.txt")
}
This set up has an assortment of potential fragile spots. For example,
dependsOn(oldWriterTask) is load-bearing, and thus if accidentally deleted in
a refactor, it would make it so that OldReader.inputFile file would sometimes
be missing depending on what tasks your user invokes. We also have to rely
on Kotlin’s lateinit for the files to make them non-null and hope that
task configuration is handled correctly.
Luckily, Gradle now has a better way to do this! Here is what tasks can look like:
abstract class NewWriter : DefaultTask() {
@get:Input
abstract val list: ListProperty<String>
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun write() = outputFile.get().asFile.writeText(
list.get().joinToString("\n")
)
}
abstract class NewReader : DefaultTask() {
@get:InputFile
abstract val inputFile: RegularFileProperty
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun writeCount() = outputFile.get().asFile.writeText(
"Line count ${inputFile.get().asFile.readLines().count()}"
)
}
One neat thing to note here, we are making our tasks and properties abstract
so we can let Gradle initialize RegularFileProperty, and ListProperty for us.
To set configure these tasks we use:
val newWriterTask = tasks.register(
"newWriter",
NewWriter::class
) {
list.addAll("apple", "orange", "banana")
outputFile.set(layout.buildDirectory.file("newWriter.txt"))
}
tasks.register("newReader", NewReader::class) {
inputFile.set(newWriterTask.flatMap { it.outputFile })
outputFile.set(layout.buildDirectory.file("newReader.txt"))
}
Using this set up, we no longer need dependsOn(newWriterTask) explicit task
dependency. newWriterTask.flatMap does the task dependency connection for us,
so when it flatMap is called to resolve the outputFile, it will run the
task to create this file. Additionally, we move away from lateinit and the
pseudo-nullness that it brings.
Similarly to the file, NewWriter can also now lazily get list that it will
write out, you can just call list.set(someProviderHere), potentially even
using flatMap as in the example above.
To summarize, any time you see dependsOn in your code, you should be eyeing it
with suspicion as there are more robust ways available to wire-up your tasks.