Chapter 44. Lazy Configuration

Table of Contents

44.1. Lazy properties
44.2. Creating a Property or Provider
44.3. Working with files and Providers
44.4. Working with task dependencies and Providers
44.5. Working with collection Providers
44.6. Guidelines
44.7. Future development
44.8. Provider API Quick Reference

As a build grows in complexity, knowing when and where a particular value is configured can become difficult to reason about. Gradle provides several ways to manage this complexity with lazy configuration.

44.1. Lazy properties

The Provider API is currently incubating. Please be aware that the DSL and other configuration may change in later Gradle versions.

Gradle provides lazy properties, which delay the calculation of a property’s value until it’s absolutely required. Lazy types are faster, more understandable and better instrumented than the internal convention mapping mechanisms. This provides two main benefits to build script and plugin authors:

  1. Build authors can wire together Gradle models without worrying when a particular property’s value will be known. For example, when you want to map properties in an extension to task properties but the values aren’t known until the build script configures them.

  2. Build authors can avoid resource intensive work during the configuration phase, which can have a direct impact on maximum build performance. For example, when a property value comes from parsing a file.

Gradle represents lazy properties with two interfaces:

Neither of these types nor their subtypes are intended to be implemented by a build script or plugin author. Gradle provides several factory methods to create instances of these types. See the Quick Reference for all of the types and factories available.

Lazy properties are intended to be passed around and only evaluated when required (usually, during the execution phase). For more information about the Gradle build phases, please see Section 22.1, “Build phases”.

The following demonstrates a task with a read-only property and a configurable property:

Example 44.1. Using a read-only and configurable property

build.gradle

class Greeting extends DefaultTask {
    // Configurable by the user
    @Input
    final Property<String> message = project.objects.property(String)

    // Read-only property
    @Internal
    final Provider<String> fullMessage = project.provider { message.get() + " from Gradle" }

    @TaskAction
    void printMessage() {
        logger.quiet(fullMessage.get())
    }
}

task greeting(type: Greeting) {
    // Note that this is effectively calling Property.set()
    message = 'Hi'
}

Output of gradle greeting

> gradle greeting
:greeting
Hi from Gradle

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

The Greeting task has a Property<String> for the mutable part of the message and a Provider for the modified, read-only, message.

Note that Groovy Gradle DSL will generate setter methods for each Property-typed property in a task implementation. These setter methods allow you to configure the property using the assignment (=) operator as a convenience.

44.2. Creating a Property or Provider

If provider types are not intended to be implemented directly by build script or plugin authors, how do you create a new one? Gradle provides various factory APIs to create new instances of both Provider and Property:

Project does not provide a specific method signature for creating a provider from a groovy.lang.Closure. When writing a plugin with Groovy, you can use the method signature accepting a java.util.concurrent.Callable parameter. Groovy’s Closure to type coercion will take care of the rest.

44.3. Working with files and Providers

In Chapter 20, Working With Files, we introduced four collection types for File-like objects:

Table 44.1. Collection of files recap

All of these types are also considered Provider types.

In this section, we are going to introduce more strongly typed models for a FileSystemLocation: Directory and RegularFile. These types shouldn’t be confused with the standard Java java.io.File type as they tell Gradle to expect more specific values (a directory or a non-directory, regular file).

Gradle provides two specialized Property subtypes for dealing with these types: RegularFileProperty and DirectoryProperty. ProjectLayout has methods to create these: ProjectLayout.fileProperty() and ProjectLayout.directoryProperty().

A DirectoryProperty can also be used to create a lazily evaluated Provider for a Directory and RegularFile via DirectoryProperty.dir(java.lang.String) and DirectoryProperty.file(java.lang.String) respectively. These methods create paths that are relative to the location set for the original DirectoryProperty.

Example 44.2. Using file and directory property

build.gradle

class FooExtension {
    final DirectoryProperty someDirectory
    final RegularFileProperty someFile
    final ConfigurableFileCollection someFiles

    FooExtension(Project project) {
        someDirectory = project.layout.directoryProperty()
        someFile = project.layout.fileProperty()
        someFiles = project.files()
    }
}

project.extensions.create('foo', FooExtension, project)

foo {
    someDirectory = project.layout.projectDirectory.dir('some-directory')
    someFile = project.layout.buildDirectory.file('some-file')
    someFiles.from project.files(someDirectory, someFile)
}

task print {
    doLast {
        def someDirectory = project.foo.someDirectory.get().asFile
        logger.quiet("foo.someDirectory = " + someDirectory)
        logger.quiet("foo.someFiles contains someDirectory? " + project.foo.someFiles.contains(someDirectory))

        def someFile = project.foo.someFile.get().asFile
        logger.quiet("foo.someFile = " + someFile)
        logger.quiet("foo.someFiles contains someFile? " + project.foo.someFiles.contains(someFile))
    }
}

Output of gradle print

> gradle print
:print
foo.someDirectory = /home/user/gradle/samples/providers/fileAndDirectoryProperty/some-directory
foo.someFiles contains someDirectory? true
foo.someFile = /home/user/gradle/samples/providers/fileAndDirectoryProperty/build/some-file
foo.someFiles contains someFile? true

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

This example shows how Provider types can be used inside an extension. Lazy values for Project.getBuildDir() and Project.getProjectDir() can be accessed through Project.getLayout() with ProjectLayout.getBuildDirectory() and ProjectLayout.getProjectDirectory().

44.4. Working with task dependencies and Providers

Many builds have several tasks that depend on each other. This usually means that one task processes the outputs of another task as an input. For these outputs and inputs, we need to know their locations on the file system and appropriately configure each task to know where to look. This can be cumbersome if any of these values are configurable by a user or configured by multiple plugins.

To make this easier, Gradle offers convenient APIs for defining files or directories as task inputs and outputs in a descriptive way. As an example consider the following plugin with a producer and consumer task, which are wired together via inputs and outputs:

Example 44.3. Implicit task dependency

build.gradle

class Producer extends DefaultTask {
    @OutputFile
    final RegularFileProperty outputFile = newOutputFile()

    @TaskAction
    void produce() {
        String message = 'Hello, World!'
        def output = outputFile.get().asFile
        output.text = message
        logger.quiet("Wrote '${message}' to ${output}")
    }
}

class Consumer extends DefaultTask {
    @InputFile
    final RegularFileProperty inputFile = newInputFile()

    @TaskAction
    void consume() {
        def input = inputFile.get().asFile
        def message = input.text
        logger.quiet("Read '${message}' from ${input}")
    }
}

task producer(type: Producer)
task consumer(type: Consumer)

// Wire property from producer to consumer task
consumer.inputFile = producer.outputFile

// Set values for the producer lazily
// Note that the consumer does not need to be changed again.
producer.outputFile = layout.buildDirectory.file('file.txt')

// Change the base output directory.
// Note that this automatically changes producer.outputFile and consumer.inputFile
buildDir = 'output'

Output of gradle consumer

> gradle consumer
:producer
Wrote 'Hello, World!' to /home/user/gradle/samples/providers/implicitTaskDependency/output/file.txt
:consumer
Read 'Hello, World!' from /home/user/gradle/samples/providers/implicitTaskDependency/output/file.txt

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

In the example above, the task outputs and inputs are connected before any location is defined. This is possible because the input and output properties use the Provider API. The output property is created with DefaultTask.newOutputFile() and the input property is created with DefaultTask.newInputFile(). Values are only resolved when they are needed during execution. The setters can be called at any time before the task is executed and the change will automatically affect all related input and output properties.

Another thing to note is the absence of any explicit task dependency. Properties created via newOutputFile() and newOutputDirectory() bring knowledge about which task is generating them, so using them as task input will implicitly link tasks together.

44.5. Working with collection Providers

In this section, we are going to explore lazy collections. They work exactly like any other Provider and, just like FileSystemLocation providers, they have additional modeling around them. They are implemented as a strongly typed model called ListProperty. You can create a new ListProperty using ObjectFactory.listProperty(java.lang.Class) and specifying the element’s type.

This type of property allows you to overwrite the entire list with Property.set(org.gradle.api.provider.Provider) or add new elements through the various add methods:

Just like every Provider, the list is evaluated when Provider.get() is called. The following example show the ListProperty in action:

Example 44.4. List property

build.gradle

task print {
    doLast {
        ListProperty<String> list = project.objects.listProperty(String)

        // Resolve the list
        logger.quiet('The list contains: ' + list.get())

        // Add elements to the empty list
        list.add(project.provider { 'element-1' })  // Add a provider element
        list.add('element-2')                       // Add a concrete element

        // Resolve the list
        logger.quiet('The list contains: ' + list.get())

        // Overwrite the entire list with a new list
        list.set(['element-3', 'element-4'])

        // Resolve the list
        logger.quiet('The list contains: ' + list.get())

        // Add more elements through a list provider
        list.addAll(project.provider { ['element-5', 'element-6'] })

        // Resolve the list
        logger.quiet('The list contains: ' + list.get())
    }
}

Output of gradle print

> gradle print
:print
The list contains: []
The list contains: [element-1, element-2]
The list contains: [element-3, element-4]
The list contains: [element-3, element-4, element-5, element-6]

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

44.6. Guidelines

This section will introduce guidelines to be successful with the Provider API. To see those guidelines in action, have a look at gradle-site-plugin, a Gradle plugin demonstrating established techniques and practices for plugin development.

  • The Property and Provider types have all of the overloads you need to query or configure a value. For this reason, you should follow the following guidelines:

    • For configurable properties, expose the Property directly through a single getter.

    • For non-configurable properties, expose an Provider directly through a single getter.

  • Avoid simplifying calls like obj.getProperty().get() and obj.getProperty().set(T) in your code by introducing additional getters and setters.

  • When migrating your plugin to use providers, follow these guidelines:

    • If it’s a new property, expose it as a Property or Provider using a single getter.

    • If it’s incubating, change it to use a Property or Provider using a single getter.

    • If it’s a stable property, add a new Property or Provider and deprecate the old one. You should wire the old getter/setters into the new property as appropriate.

44.7. Future development

Going forward, new properties will use the Provider API. The Groovy Gradle DSL adds convenience methods to make the use of Providers mostly transparent in build scripts. Existing tasks will have their existing "raw" properties replaced by Providers as needed and in a backwards compatible way. New tasks will be designed with the Provider API.

The Provider API is incubating. Please create new issues at gradle/gradle to report bugs or to submit use cases for new features.

44.8. Provider API Quick Reference