Skip to content
Book Cover of Extending Android Builds

4-1: Gradle Lifecycle Hooks ​

Picking up from our previous sharing in Section 2-2 on lifecycles, we're going to delve into the Hook mechanism provided by Gradle. Hook functions allow developers to tap into specific points of the build process. It's vital to note that these hooks operate at a global level and are not merely about appending logic to a single Task or Action.

You'll find the sample code for this section in the gradle-lifecycle project. We won't be reproducing complete hook codes here due to the fragmented nature of the their invocations. However, to see them in action, execute ./gradlew clean -I init.gradle.kts. This will allow you to monitor the output and witness the timing of hook usage in relation to where the lifecycle class from build-logic is invoked. For clarity, please revisit the overview diagram of Gradle Lifecycle in Section 2-2 (Figure 2.2.1).

4-1-1: Categorizing the Hooks ​

The realm of Gradle's lifecycle hooks is vast, and there's no official classification. I've taken the liberty to categorize them into three groups based on the caller types and accepted parameters for each hook API.

For Context Objects with Action ​

First up, we have hooks that can be directly invoked through key context objects. Examples include Gradle#afterProject(...), Project#afterEvaluate(...), and TaskGraph#beforeTask(...). The overview provided above is primarily based on these hooks. Characteristically, these hooks accept Action (or Closure) as parameters. This means you can instantly pass a closure to monitor outcomes. Although they're straightforward to use, their APIs are a bit scattered, requiring interaction with multiple context objects.

Kotlin
gradle.taskGraph.whenReady {
  Lifecycle.whenTaskGraphIsReady("whenReady")
}
...
project.beforeEvaluate {
  Lifecycle.beforeEvaluate(this.displayName)
}
...
tasks.whenTaskAdded {
    Lifecycle.whenTaskAdded(proj.name + ":" + this.name)
}

The forthcoming segments will shed more light on the utility and functions of each hook.

Listener Callbacks ​

Listeners provide a powerful mechanism to tap into various stages of the build process. Here, we'll focus on two primary methods to add listeners and the range of available listeners they accommodate.

Method to Add ListenersListeners Offered
Gradle#addListener(...)1. TaskExecutionGraphListener: graphPopulated
2. TaskExecutionListener: beforeExecute, afterExecute
3. TaskExecutionAdapter: beforeExecute, afterExecute
4. TaskActionListener: beforeActions, afterActions
Gradle#addBuildListener(...)BuildListener: settingsEvaluated, projectsLoaded, projectsEvaluated, buildFinished

The name and function of most listeners mirror those of the first category (Context Object with Action) . The process of configuring other listeners follows a familiar pattern. For more practical implementations, we'll delve deeper into hook usage scenarios later on.

Kotlin
gradle.addBuildListener(object : BuildListener {
  override fun settingsEvaluated(settings: Settings) {
    Lifecycle.onSettingsEvaluated(...)
  }

  override fun projectsLoaded(gradle: Gradle) {
    Lifecycle.onProjectsLoaded(...)
  }

  override fun projectsEvaluated(gradle: Gradle) {
    Lifecycle.onProjectsEvaluated(...)
  }

  override fun buildFinished(result: BuildResult) {
    Lifecycle.onBuildFinished(...)
  }
})

Contrasting the two API types, the second offers a broader perspective. It grants callback parameters that present context object information relevant to that stage. However, it doesn't offer the same granularity as the first type. These API types can be seen as complementary.

Event Notifications ​

The third category brings us to event notifications. These are similar in essence to listeners, designed to alert of various events during the build process. In light of the Configuration Cache (refer to Section 4-5), several APIs from both the first and second types, such as taskGraph.beforeTask and gradle.buildFinished() during the task execution phase (Execution), have been labeled as incompatible. Some have received the @Deprecated tag. This newly introduced event notification API emerges as a potential substitute for the now-deprecated ones.

To register for these event notifications, employ the BuildEventsListenerRegistry. Then implementing the OperationCompletionListener provides the listening mechanism. As of now, the BuildEventsListenerRegistry offers a singular API, onTaskCompletion(Provider<? extends OperationCompletionListener> listener), dedicated to notifying when a task completes. Gradle's documentation nudges us towards the Shared Build Services for creating these listeners. For a more in-depth look into this, head over to Section 4-4.

Kotlin
// Leveraging the Gradle's built-in dependency injection 
// to fetch the `BuildEventsListenerRegistry`.
class BuildEventsListenerRegistryPlugin @Inject constructor(
  private val registry: BuildEventsListenerRegistry
) : Plugin<Settings> {
  override fun apply(settings: Settings) {
    settings.gradle.run {
      val buildFinishTrackService = sharedServices.registerIfAbsent(
        "buildFinishTrackService", BuildFinishTrackService::class) {
        parameters {}
      }
      registry.onTaskCompletion(buildFinishTrackService)
    }
  }
}

abstract class BuildFinishTrackService : BuildService<Parameters>,
  OperationCompletionListener {
  ...
  override fun onFinish(e: FinishEvent) {
    if (e is TaskProgressEvent) {
      when (e) {
        is TaskStartEvent -> {
          Lifecycle.afterTask("TaskStartEvent")
        }

        is TaskFinishEvent -> {
          Lifecycle.afterTask("TaskFinishEvent")
          when (val res = e.result) {
            is SuccessResult -> {
              Lifecycle.afterTask(
                "${e::class.simpleName} >>> ${res::class.simpleName} "
                    + ">>> Res: Success. >>> EndTime: ${res.endTime}."
              )
              val desc: TaskOperationDescriptor = e.descriptor
              Lifecycle.afterTask("taskPath: ${desc.taskPath}")
              // Lifecycle.afterTask(desc.dependencies.first().displayName)
              // Lifecycle.afterTask("${desc.taskPath} belongs to " +
              //   "${desc.originPlugin ?: "Unknown"}")
            }

            is SkippedResult -> {
              Lifecycle.afterTask(...)
            }

            is FailureResult -> {
              Lifecycle.afterTask(...)
            }
          }
        }
        ...
      }
    }
  }
  ...
}

Gaining insights from a task event primarily revolves around the attribute fields present within the TaskOperationDescriptor. Yet, beyond the timing metrics, only the taskPath is practically useful. Even though fields like dependencies and originPlugin have public visibility and the documentation mentions their availability since Gradle 5.1, invoking them via external Plugins will unfortunately throw an UnsupportedMethodException.

Additionally, the taskPath provides a comprehensive task name, e.g., :lib:preDebugBuild. To adeptly parse this, a glimpse into AGP's monitoring code can be helpful. Let's look at a few crucial functions derived from there:

Kotlin
// For a full view, check the following classes:
// com.android.build.gradle.internal.profile.AnalyticsService
// com.android.build.gradle.internal.profile.AnalyticsResourceManager

fun recordTaskExecutionSpan(finishEvent: FinishEvent?) {
  if (finishEvent == null || finishEvent !is TaskFinishEvent) return
  val taskPath = finishEvent.descriptor.taskPath
  val taskRecord = getTaskRecord(taskPath) ?: return
  val typeName = getTypeName(taskPath) ?: return
  val taskType = getTaskExecutionType(typeName)
  val taskResult = finishEvent.result
  ...
}

fun getTaskRecord(taskPath: String) : TaskProfilingRecord? {
  if (!taskRecords.containsKey(taskPath)) {
    ...
    val projectPath = getProjectPath(taskPath) ?: return null
    val variantName = getVariantName(taskPath)
    taskRecords[taskPath] = ...
  }
  return taskRecords[taskPath]  // [I]
}

fun collectTaskMetadata(graph: TaskExecutionGraph) {
  graph.allTasks.forEach { task ->
    val variantName = if (task is VariantAwareTask) {
      task.variantName
    } else {
      task.extensions.findByName(PROPERTY_VARIANT_NAME_KEY) as String?
    }
    taskMetadata[task.path] = TaskMetadata(
      task.project.path,
      variantName,
      AnalyticsUtil.getPotentialTaskExecutionTypeName(task.javaClass))
  }
}

private fun getProjectPath(taskPath: String) : String? = 
  taskMetadata[taskPath]?.projectPath
private fun getVariantName(taskPath: String) : String? = 
  taskMetadata[taskPath]?.variantName
private fun getTypeName(taskPath: String) : String? = 
  taskMetadata[taskPath]?.typeName

This offers a good look into the role of the OperationCompletionListener in task analysis. For example, we can find a detailed record of the Task with getTaskRecord(...) (at [I]).

What's more, setting up a BuildService isn't strictly required. When we avoid handling Task listening in Settings and move it to Project, the BuildEventsListenerRegistry instance can be retrieved through the inherent ObjectFactory by Gradle's Managed Classes/Properties features.

Kotlin
abstract class Listener : OperationCompletionListener {
  override fun onFinish(event: FinishEvent) {
    println("onTaskFinish: $event")
  }
}

interface RegistryProvider {
  @get:Inject
  val registry: BuildEventsListenerRegistry
}

objects.newInstance<RegistryProvider>().registry
  .onTaskCompletion(provider { objects.newInstance<Listener>() })

Beyond the Task events, Gradle has laid the foundation for a myriad of other notification events, nestled under the org.gradle.tooling.events package.

Kotlin
import org.gradle.tooling.events.configuration.ProjectConfigurationProgressEvent
import org.gradle.tooling.events.download.FileDownloadProgressEvent
import org.gradle.tooling.events.task.TaskProgressEvent
import org.gradle.tooling.events.test.TestProgressEvent
import org.gradle.tooling.events.transform.TransformProgressEvent
import org.gradle.tooling.events.work.WorkItemProgressEvent

From the above, besides the TaskProgressEvent, we can spot five more public XxxProgressEvent classes, all of which have a structure akin to the task event. Although, there isn't currently an entry point that supports their reception, you may notice these events are part of the Gradle Tooling API, sharing the same foundational architecture as the Gradle Scan Plugin probably. So, it's worth keeping an ear to the ground as we might witness more expansive event support in upcoming Gradle versions.

While the event notification API can replicate certain functionalities, especially the monitoring capability of the second API set when used in afterTask, it's pivotal to remember that the gradle.buildFinished() of the first API is deprecated. However, there's a nifty workaround:

Kotlin
abstract class BuildFinishTrackService : BuildService<Parameters>,
  OperationCompletionListener, AutoCloseable {
  ...
  override fun close() {
    Lifecycle.onBuildFinished("Triggered by AutoCloseable.")
  }
}

We've adapted the BuildFinishTrackService by incorporating the AutoCloseable interface and, subsequently, the close() method. If you delve into the documentation, it explains that the BuildService feature brings the service to a close at the right moment through the JDK's AutoCloseable interface. This is typically sandwiched between the completion of relevant tasks and the end of the build. Given the registration setup of OperationCompletionListener, our service is activated without fail and persists until the final task wraps up. Therefore, this provides a seamless alternative to the buildFinished() method.

4-1-2: Hooks in the Initialization Phase ​

gradle.buildStarted(...) ​

Kicking off, we have the gradle.buildStarted{} hook. However, it doesn't spring into action for developers working outside the Gradle source code. Because the callback fires too early in the processβ€”following the creation of the Gradle object but prior to the initialization of Settings and each Project object. It also beats the trigger of any external scripts, even init.gradle(.kts).

Due to these characteristics, this interface was flagged as deprecated in Gradle 6.x and was axed in version 7.0 to avoid muddying the waters for developers.

gradle.settingsEvaluated(...) ​

This hook jumps into action after the execution of settings.gradle(.kts).

java
public class SettingsEvaluatedCallbackFiringSettingsProcessor ... {
  ...
  @Override
  public SettingsInternal process(GradleInternal gradle,
                  SettingsLocation settingsLocation,
                  ClassLoaderScope buildRootClassLoaderScope,
                  StartParameter startParameter) {
    SettingsInternal settings = delegate.process(gradle,
      settingsLocation, buildRootClassLoaderScope, startParameter);
    gradle.getBuildListenerBroadcaster().settingsEvaluated(settings);
    settings.preventFromFurtherMutation();
    return settings;
  }
}

Standard practice is to register this callback either within the Plugins imported via the init.gradle(.kts) script or directly within the settings.gradle(.kts). A closer inspection of the source code reveals that this hook's timing aligns almost perfectly with the conclusion of settings.gradle(.kts). Consequently, in typical projects devoid of any Init script or other Plugins in settings.gradle(.kts), functions intended for this hook can be placed at the tail end of settings.gradle(.kts). Here are some handy uses of this hook:

  1. Tweaking ProjectDescription: After using include(String name) in settings.gradle(.kts), Projects take their place in the rootProject of Settings, flaunting their ProjectDescription. Here's a snapshot of the ProjectDescription API:

Figure 4.1.1: The Project APIs

In Section 2-2, we showcased how to modify buildFileName to declutter the build.gradle(.kts) display. You're also empowered to employ the ProjectDescription API to switch compilation files across diverse environments or pinpoint specific project paths when ushering in external modules.

  1. Decreeing a universal buildCache setup: Consider a scenario where an organization allots each Gradle Project a remote buildCache space. Leveraging the standardized init.gradle(.kts) script, the pertinent buildCache setup can be appended to the Settings object right after settings.gradle(.kts) concludes its execution. Check out more buildCache related commands in Section 4-5.

gradle.projectsLoaded(...) ​

After the Settings initialization, once the ProjectDescription is recognized, Gradle promptly loads the pertinent Module as defined by the ProjectDescription. This then leads to the creation of the Project object. The gradle.projectsLoaded{} hook is tied to the Gradle object. Within its closure, we can access the loaded Project's root node via rootProject, enabling traversal and access to all underlying Projects. Primarily, this callback highlights actions required before the Project's build script gets executed. Some typical uses are:

  • Registering lifecycle listeners for the Project: This should ideally occur after the Project object has been instantiated but before its execution. While listeners for non-root Projects can be specified within the rootProject, specific rootProject listeners (like beforeProject(), rootProject.tasks.whenTaskAdded(), and so on) can only be defined within the projectsLoaded hook.
  • Introducing buildscript configuration to the Project. This includes the addition of dependencies, Plugins, and so on. As outlined in the preceding callback, using the init.gradle(.kts) script within the production CI environment allows for global Plugin integration. For instance, incorporating a performance analytics Plugin can be achieved timely through this callback, accessing each Project object and adding more custom logic.

4-1-3: Hooks in the Configuration Phase ​

gradle.beforeProject(...) ​

This hook largely mirrors its predecessor gradle.projectsLoaded(...), encompassing tasks like adding supplementary dependencies and Plugins. Here's what sets it apart:

  • For the gradle.projectsLoaded() callback, traversing from rootProject is necessary to access the desired Project from the provided Gradle object.
  • gradle.beforeProject() is triggered prior to every Project's execution, passing in the respective Project object.

project.beforeEvaluated(...) ​

Though this hook and the one before it share the same trigger timing, they interact with distinct objects. While gradle.beforeProject() offers a blanket setup for all Projects, project.beforeEvaluated{} mandates individual setups for each Project. Key takeaways are:

  • If there's a need to configure a specific Project only, this hook provides a more streamlined operation.
  • In the sequence of operations, gradle.beforeProject(...) takes precedence, followed by project.beforeEvaluated(...).

gradle.afterProject(...) ​

This hook pairs with the gradle.beforeProject(...). For its extensive utilization, one can look directly into the project.afterEvaluated(...).

project.afterEvaluated(...) ​

The project.afterEvaluated(...) hook is commonly used in many Gradle scripts. On the one hand, since the DSL Extension and task graph aren't fully accessible during the configuration phase, this hook is typically used to read configurations in build.gradle(.kts). On the other hand, it also offers an advanced configuration opportunity for tasks that need the above extension data. A prime example is the Variant-related configuration in AGP, including the beforeVariants(...) and onVariants(...) discussed in Section 3-3, all of which occur within the AGP's preset afterEvaluated(...).

When an Android project introduces multiple Plugins, these could register numerous afterEvaluated(...) callbacks. Given that these computations occur during the configuration phase, inclusive of the IDE sync process, it's imperative to use this hook judiciously to prevent potential performance issues.

Future chapters will showcase various uses of this hook. And in Section 8-1, we'll illustrate how to view all instances of afterEvaluate(...) via the Gradle Scan Report.

gradle.projectsEvaluated(...) ​

This hook, akin to gradle.projectsLoaded(...), activates right before task execution. Its usage closely mirrors that of the project.afterEvaluated(...), but its activation is post the completion of all Project evaluations rather than a singular project.afterEvaluate(...). This hook can also be seen as a global Project monitoring tool, allowing for data aggregation.

taskGraph.whenReady(...) ​

Reflecting on our deep dive into the TaskExecutionGraph in Section 2-2, this hook comes alive post the successful POPULATED state. Consider it a final touchpoint for task configuration. Once all executable tasks are organized, Gradle grants an opportunity to modify them. For instance, you might want to disable certain tasks not required for the specific Variant.

4-1-4: Hooks in the Execution Phase ​

taskGraph.beforeTask(...) ​

This hook, living up to its name, provides a last-minute opportunity to tweak a task before its execution. One potential use-case is to inject APM (Application Performance Management) logging code in tandem with afterTask(...).

However, a word of caution is needed. Consider a hypothetical situation where the AGP has two intertwined tasks, A and B. If B relies on the output of A and you decide to intercept B's execution, changing its content just before its passage to B, it might seem to fulfill specific requirements. Yet, this approach is not compatible with Gradle's caching mechanism (the up-to-date check). A more refined strategy would involve placing a custom Task C between A and B, defining clear input and output parameters.

Do note that, post Gradle 7, this mechanism is also not compatible with Configuration Cache and is now deprecated.

taskActionListener.beforeAction(...) ​

By its name, this hook is initiated just before any Task Action is executed. However, its practical application remains relatively rare. It's important to highlight that this mechanism doesn't support Configuration Cache and was deprecated in Gradle 7.

taskActionListener.afterAction(...) ​

Similar to its "before" counterpart, this hook activates after any Task Action's execution. Its real-world application is sparse. Again, as with the previous hook, it's unsupported by Configuration Cache and deprecated as of Gradle 7.

taskGraph.afterTask(...) ​

This hook springs into action subsequent to any Task's execution. Commonly, it's paired with beforeTask(...) for the insertion of APM logging. Importantly, this hook remains active regardless of compilation results or error instances.

Furthermore, there exists an alternate set of listeners, as demonstrated below. These parallel the beforeTask(...) and afterTask(...) functionalities.

Kotlin
gradle.taskGraph.addTaskExecutionListener(
  object : TaskExecutionListener {
    override fun beforeExecute(task: Task) {}
    override fun afterExecute(task: Task, state: TaskState) {}
  })

However, post Gradle 7, this feature lacks Configuration Cache support and is on the deprecation list.

gradle.buildFinished(...) ​

This hook is the curtain call, activated once all Gradle build logic concludes. It's predominantly utilized for concluding tasks such as:

  • Purging superfluous intermediate files on the CI/CD server.
  • Aggregating a comprehensive report of the compilation process and forwarding it to a remote server.

This hook is steadfast in its operation, unaffected by compilation outcomes or potential errors. However, as with many hooks, post Gradle 7, their compatibility with Configuration Cache is terminated. Check the "Event Notification" portion in this section for its alternative.

4-1-5: In Summary ​

The intricacies of Gradle's lifecycle hooks warrant careful attention. It's essential to grasp their potential impact, especially concerning the Configuration phase. As we transition beyond Gradle 7, the array of deployable hooks has dwindled. The newer Event Notification model remains in anticipation of further API extensions.