[Gradle-8] Gradle plug-in development guide

1 Introduction

Gradle plug-in development occupies a certain proportion in advanced Android knowledge, especially in the field of performance optimization, which is basically covered, and is also closely related to our daily compilation and packaging. In addition, many recruitment requirements clearly require Gradle plug-ins. Development experience, so even though most people may not use plug-in development in their daily development, they still yearn for it. This article brings you the 8th article in the Gradle series - Plug-in Development Guide, and I hope it will bring you benefits.

2. What is the Gradle plug-in?

Gradle plug-in (Plugin) is a mechanism for extending and customizing the functionality of the Gradle build system. Gradle is a powerful build automation tool for building and managing various types of projects, from simple Java applications to complex multi-module enterprise projects. Plugins provide flexibility to Gradle, allowing developers to add custom behavior and functionality based on specific needs.
Gradle plugins can perform a variety of tasks, including compiling code, executing tests, packaging files, generating documentation, and more. Plug-ins can access and operate Gradle's build models, such as projects, tasks, dependencies, etc., to control and customize the build process.
Gradle provides a rich plugin ecosystem, and you can use existing official plugins or third-party plugins to enhance the build process. Many popular frameworks and tools, such as Android, Spring Boot, Kotlin, etc., have corresponding Gradle plug-ins, making integration with these technology stacks easier and more efficient.
For example, the familiar Android plug-in com.android.application:

plugins {
    
    
    id 'com.android.application'
}

By writing your own Gradle plugin, you can customize and extend the Gradle build system to fit the needs of your specific project. You can define custom tasks, configure extensions, operate project properties, apply other plug-ins, etc. in the plug-in. Plug-ins make the build process controllable and customizable, thereby improving development efficiency.

3. Why write plug-ins?

The meaning of writing plug-ins:

  1. 封装, extracting the specific logic, the project only needs to run the plug-in, and there is no need to put it in a certain build.gradle file, which will reduce the readability of build.gradle;
  2. 复用, extract the common logic, and when using it, you only need to apply the plug-in. You don’t need to copy it over and over again, and it can also be provided to other projects for use;
  3. 定制: If you need to do some custom operations such as instrumentation and Hook during compilation, you also need to use the compilation plug-in;

4. Where is the plug-in written?

We introduced Gradle Task above , and mentioned where the Task is written, but where is the Plugin written?
Plugin can be written in 3 places:

  1. Like Task, it is written in the build.gradle file and scopes the current Project;
  2. Written in buildSrc, the scope is all Projects in the current project;
  3. Written in a separate project, it can be provided to all projects after release;

According to your own needs, combined with the plug-in scope, you can write it in different locations.

5. Custom plug-ins

Writing a plug-in Plugin is actually quite simple. You only need to implement Pluginthe interface and the only applymethod.
We just write it directly in build.gradlethe file:

class YechaoaPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
    }
}

apply plugin: YechaoaPlugin
//apply(plugin: YechaoaPlugin)

This is actually an inline class.

Don’t forget to apply dependencies after writing. The apply method on line 9 is the method of the PluginAware� interface called apply(), and the parameter is one map, which is used to map the Plugin ID.
sync output:

> Configure project :app
这是插件:YechaoaPlugin
...

As mentioned in the detailed explanation of Task in the previous article , Task is a method in Project, so we need to create a Task through Project. In the example, the YechaoaPlugin class implements the Plugin interface and implements the only apply method. The apply method provides a Project object, so we can also create a Task in the Plugin.

class YechaoaPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
        project.task("YechaoaPluginTask") { task ->
            task.doLast {
                println("这是插件:${this.class.name},它创建了一个Task:${task.name}")
            }
        }
    }
}

As above, we created a Task in the Plugin. At this time, sync will not execute the printing in the Task, and the Task must be executed separately.
implement:

./gradlew YechaoaPluginTask

Output:

> Task :app:YechaoaPluginTask
这是插件:YechaoaPlugin,它创建了一个Task:YechaoaPluginTask

OK, the most basic Plugin writing is that simple.

Combining the above two outputs, whether it is simply printing in the Plugin or creating a Task in the Plugin, when we rely on the YechaoaPlugin plug-in, that is, this apply will put the plug-in apply plugin: YechaoaPluginin PluginContainer�, similar to the TaskContainer, and at the same time this apply The apply() method of the Plugin interface is also executed during the compilation phase, so there will be output after sync is executed and built, and the executed Task is also in the directed acyclic graph.

6. Custom plug-in extensions

In the second chapter of the Gradle series , we analyzed android{ }how closures come from the source code. android{ }Closures are a very familiar configuration. Through DSL, we often configure compileSdk�, buildTypes�, etc. in it.
When customizing plug-ins, there is often a need for this kind of custom configuration. Through these custom configurations, our plug-ins can provide richer capabilities. These configurations are provided through extension plug-ins.

6.1. Define extension objects

interface YechaoaPluginExtension{
    Property<String> getTitle()
}

It can be an interface or a class.

6.2. Add the extension to the Plugin and use it

class YechaoaPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
        def extension = project.extensions.create("yechaoa", YechaoaPluginExtension)
        project.task("YechaoaPluginTask") { task ->
            task.doLast {
                println("这是插件${this.class.name},它创建了一个Task:${task.name}")
                println(extension.title.get())
            }
        }
    }
}

project.extensions.create()The method receives two parameters:

  1. The first is the name, such as yechaoa, android;
  2. The second is to extend the object, and then return this extension object. You can obtain customized configuration parameters through the methods of this extension object;

6.3. Configuration parameters

yechaoa.massage = "【Gradle-8】Gradle插件开发指南"

A configuration can be omitted directly, or it can be written like this

yechaoa {
  	massage = "【Gradle-8】Gradle插件开发指南"
}

If no configuration parameters are set, Gradle also provides default value settings:

extension.title.convention("默认配置title")

If it is a class object, define the setter/getter.

How to write if there are multiple configurations? Just extend multiple configuration properties.

6.4. Nested expansion

As shown below, there is also defaultConfig { } in android { }

android {
    namespace 'com.yechaoa.gradlex'
    compileSdk 32

    defaultConfig {
        applicationId "com.yechaoa.gradlex"
        ...
    }
}

Nested expansion is actually very simple, just like a matryoshka doll.

Above we used the interface to define the extended attributes. Let's change the writing method and use the class object to define it.

6.4.1. Definition extension

class YechaoaPluginExtension {
    String title
    int chapter
    SubExtension subExtension

    YechaoaPluginExtension(Project project) {
        subExtension = project.extensions.create('sub', SubExtension.class)
    }
}
class SubExtension {
    String author
}

Define an extra SubExtension class and add it to YechaoaPluginExtension when it is instantiated ExtensionContainer�.

If you want to nest classes, that's fine. They have to be inline classes, otherwise the compiler won't be able to recognize them.

6.4.2. Obtain extended attributes

class YechaoaPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
        def extension = project.extensions.create("yechaoa", YechaoaPluginExtension)
        // 设置默认值 可以定义set()方法 然后在这里set
        project.task("YechaoaPluginTask") { task ->
            task.doLast {
                println("这是插件${this.class.name},它创建了一个Task:${task.name}")
                println("title = ${extension.title}")
                println("chapter = ${extension.chapter}")
                println("author = ${extension.subExtension.author}")
            }
        }
    }
}

Compared with the above example of interface definition, there are no Propertyobjects and the setting of default values ​​has been removed. If you want, you can just .get()define the method in the class object , and the other logic remains unchanged.setter/getter

6.4.3. Use

yechaoa {
    title = "【Gradle-8】Gradle插件开发指南"
    chapter = 8
    sub {
        author = "yechaoa"
    }
}

In the closure configuration, there is an additional sub{ }closure, which is defined in our YechaoaPluginExtension class.

6.4.4. Execution

./gradlew YechaoaPluginTask

6.4.5. Output

> Task :app:YechaoaPluginTask
title = 【Gradle-8】Gradle插件开发指南
chapter = 8
author = yechaoa

6.4.6. Complete code

class YechaoaPluginExtension {
    String title
    int chapter
    SubExtension subExtension

    YechaoaPluginExtension(Project project) {
        subExtension = project.extensions.create('sub', SubExtension.class)
    }
}
class SubExtension {
    String author
}

class YechaoaPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println("这是插件:${this.class.name}")
        def extension = project.extensions.create("yechaoa", YechaoaPluginExtension)
        // 设置默认值 可以定义set()方法 然后在这里set
        project.task("YechaoaPluginTask") { task ->
            task.doLast {
                println("这是插件${this.class.name},它创建了一个Task:${task.name}")
                println("title = ${extension.title}")
                println("chapter = ${extension.chapter}")
                println("author = ${extension.subExtension.author}")
            }
        }
    }
}

apply plugin: YechaoaPlugin

yechaoa {
    title = "【Gradle-8】Gradle插件开发指南"
    chapter = 8
    sub {
        author = "yechaoa"
    }
}

yechaoa{ }Does this configuration look familiar now :

yechaoa {
    title = "【Gradle-8】Gradle插件开发指南"
    chapter = 8
    sub {
        author = "yechaoa"
    }
}

Is it the android{ }same as a dime:

android {
    namespace 'com.yechaoa.gradlex'
    compileSdk 32

    defaultConfig {
        applicationId "com.yechaoa.gradlex"
        ...
    }
}

7. Written in a separate project

Our Plugin above is written in build.gradlea file, but generally in actual projects, for better reuse, it is usually written in buildSrc or a separate project.
There are some differences between writing in the build.gradle file and writing in buildSrc or a separate project. Let's take a look at how to write it in a separate project (equal to buildSrc).

To make it simple, just write a Plugin that prints all dependencies in the project.

7.1. Create new Module

Create a new Module named plugin, and select the type as Libraryor below Java or Kotlin Library
New module.png
. After creating a new Module, there will be a default file directory, and redundant files can be deleted.
Default directory.png
We can see that there is a java folder under the main folder. Gradle Plugin can be written in java, or it can be learned using kotlin or groovy. You can create a new folder interface corresponding to the language under the main file, such as the kotlin folder. .

7.2. Create a new file and add dependencies

7.2.1. Create a new class

Create a new DependenciesPlugin� class:
image.png
But you still cannot write Plugin at this time, because your module does not rely on Gradle-related APIs.

7.2.2. Add dependencies

In the future , Gradle 6.4there is no need to add gradleApi() to configure Plugin dependencies. java-gradle-pluginIt can be done directly with a plug-in, which will automatically add dependencies to the project. And you don’t need to configure yours in java as before . Just configure it with gradlePlugin. Gradle will automatically generate the description file.javagradleApi()
src/main/resources/META-INF/gradle-plugins/xxx.propertiesimplementation-classMETA-INF

Depend on the plugin in the plugin>build.gradle file:

plugins {
    id 'java-gradle-plugin'
}

The configuration is as follows:

gradlePlugin{
    plugins{
        DependenciesPlugin{
            id = 'com.yechaoa.plugin.dependencies'
            implementationClass = 'com.yechaoa.plugin.DependenciesPlugin'
        }
    }
}
  • id�: plugin id referenced when applying;
  • implementationClass:Plugin path;

Before Gradle 6.4:

implementation-class=com.yechaoa.plugin.DependenciesPlugin

Because in the past, these folders and configurations were all manual, which was very cumbersome. In comparison, it is much more enjoyable now.

7.3. Write Plugin

package com.yechaoa.plugin;


import org.gradle.api.Plugin;
import org.gradle.api.Project;

/**
 * GitHub : https://github.com/yechaoa
 * CSDN : http://blog.csdn.net/yechaoa
 * <p>
 * Created by yechao on 2023/8/8.
 * Describe :
 */
class DependenciesPlugin implements Plugin<Project> {
    
    

    @Override
    public void apply(Project project) {
    
    
        System.out.println(">>>>>>>>  " + this.getClass().getName());
    }
}
  1. Create a new class to implement the Plugin interface;
  2. Implement your own logic in the apply method, as shown in the example below;

At this point, the basic prototype of the Plugin is available.

Add dependencies using:

apply plugin: 'com.yechaoa.plugin.dependencies'

But now it cannot be used in external projects. If you directly reference this, the plugin will not be found (not found).

Plugin with id 'com.yechaoa.plugin.dependencies' not found.

Because this Plugin is written in a separate project, to be precise, it has nothing to do with other projects. If you want to find this plug-in, you must publish it.

7.4. Local release

Local publishing is much simpler than remote publishing. Although remote publishing is not difficult, it is just cumbersome.

7.4.1. Maven plug-in

First of all, the more commonly used warehouse is maven. In the plugin>build.gradle file, first rely on a plug-in released by maven.'maven-publish'

plugins {
    
    
    id 'maven-publish'
}

dependencies {
    
    
    implementation 'com.android.tools.build:gradle:7.3.0'
}

7.4.2. Release configuration

Add release configuration

group 'com.yechaoa.plugin'
version '1.0.0'

publishing {
    
    
    // 配置Plugin GAV
    publications {
    
    
        maven(MavenPublication) {
    
    
            groupId = group
            artifactId = 'dependencies'
            version = version

            from components.java
        }
    }
    // 配置仓库地址
    repositories {
    
    
        maven {
    
    
            url layout.buildDirectory.dir("maven-repo")
        }
    }
}

7.4.3. Perform publishing operation

./gradlew publish

Or click Run in the Gradle visualization panel on the right side of Android Studio publish:
publish.png

7.4.4. Generate products

unwanted.png
OK, now there is maven-repoa folder for local release configuration under the build folder.
You can confirm the maven metadata and pomfiles again:

<metadata>
  <groupId>com.yechaoa.plugin</groupId>
  <artifactId>dependencies</artifactId>
  <versioning>
    <latest>1.0.0</latest>
    <release>1.0.0</release>
    <versions>
      <version>1.0.0</version>
    </versions>
    <lastUpdated>20230809154815</lastUpdated>
  </versioning>
</metadata>

7.5. Use

Ok, the local release is completed. If you want to use this plug-in, the process is the same as our normal dependence on plug-ins.
Three steps:

  1. Configure the plug-in warehouse address in the settings.gradle file
pluginManagement {
    
    
    repositories {
    
    
        // ...
        maven {
    
    
            url './maven-repo'
        }
    }
}
  1. Add plugin dependencies in the project>build.gradle file
buildscript {
    
    
    dependencies {
    
    
        classpath('com.yechaoa.plugin:dependencies:1.0.0')
    }
}
  1. Depend on our plugin in the app:build.gradle file
plugins {
    
    
    id 'com.yechaoa.plugin.dependencies'
}

The above configurations are all added in the app module, which is the module that needs to be used.

Compile to see the effect:

> Configure project :app
>>>>>>>>  com.yechaoa.plugin.DependenciesPlugin

ok, it is printed correctly, indicating that our customized plugin is ready for external use.

Note: When using local dependencies, they must be published first and then depend on the plug-in, otherwise there will be a situation where the dependencies cannot be found.

7.6. Function implementation

The above example is just a print, continue to implement our function and print out all dependencies.

There are many ways to print dependencies, such asgradle命令

./gradlew app:dependencies

So what if I want to distinguish between official libraries and third-party libraries? This is no longer satisfactory.

Let’s modify the above Plugin:

class DependenciesPlugin implements Plugin<Project> {
    
    

    private final String TAG = "DependenciesPlugin >>>>> ";

    @Override
    public void apply(Project project) {
    
    
        System.out.println(TAG + this.getClass().getName());

        DependenciesPluginExtension extension = project.getExtensions().create("printDependencies", DependenciesPluginExtension.class);

        project.afterEvaluate(pro -> {
    
    

            /*
             * 扩展的配置要在 project.afterEvaluate 之后获取哦
             * 因为配置阶段完成,才能读取参数
             * 且配置完成,才能拿到所有的依赖
             */

            // 默认开启打印
            extension.getEnable().convention(false);

            if (extension.getEnable().get()) {
    
    
                // debug/release也可以加配置
                System.out.println(TAG + "已开启依赖打印");

                AppExtension androidExtension = project.getExtensions().getByType(AppExtension.class);

                androidExtension.getApplicationVariants().all(applicationVariant -> {
    
    
                    System.out.println(TAG + ">>>>>>>>  applicationVariant.getName() = " + applicationVariant.getName());
                    // 方式一:build.gradle 文件中添加的依赖
                    Configuration configuration = project.getConfigurations().getByName(applicationVariant.getName() + "CompileClasspath");
                    Set<Dependency> allDependencies = configuration.getAllDependencies();
//                for (Dependency dependency : allDependencies) {
    
    
//                    System.out.println(TAG + "dependency === " + dependency.getGroup() + ":" + dependency.getName() + ":" + dependency.getVersion());
//                }

                    List<String> androidLibs = new ArrayList<>();
                    List<String> otherLibs = new ArrayList<>();

                    // 方式二:所有的依赖,包括依赖中的依赖
                    configuration.getResolvedConfiguration().getLenientConfiguration().getAllModuleDependencies().forEach(resolvedDependency -> {
    
    
                        ModuleVersionIdentifier identifier = resolvedDependency.getModule().getId();
                        //System.out.println(TAG + "identifier === " + identifier.getGroup() + ":" + identifier.getName() + ":" + identifier.getVersion());
                        if (identifier.getGroup().contains("androidx") || identifier.getGroup().contains("com.google") || identifier.getGroup().contains("org.jetbrains")) {
    
    
                            androidLibs.add(identifier.getGroup() + ":" + identifier.getName() + ":" + identifier.getVersion());
                        } else {
    
    
                            otherLibs.add(identifier.getGroup() + ":" + identifier.getName() + ":" + identifier.getVersion());
                        }
                    });

                    System.out.println("--------------官方库 start--------------");
                    androidLibs.forEach(System.out::println);
                    System.out.println("--------------官方库 end--------------");

                    System.out.println("--------------三方库 start--------------");
                    otherLibs.forEach(System.out::println);
                    System.out.println("--------------三方库 end--------------");
                });
            } else {
    
    
                System.out.println(TAG + "已关闭依赖打印");
            }
        });

    }
}

Extensions:

interface DependenciesPluginExtension {
    
    
    Property<Boolean> getEnable();
}

use:

printDependencies {
    
    
    enable = true
}

summary:

  1. First, a configuration enable is added to determine whether printing dependencies are needed;
  2. After the project evaluation is completed (project.afterEvaluate�), obtain the project configuration (Configuration)�;
  3. Get all dependencies through Configuration (getAllModuleDependencies);
  4. Traverse to obtain GAV and classify;
  5. Finally print it out;

One thing to note here is that we need to obtain the extension configuration in the project.afterEvaluate method, because the execution time of apply plugin is earlier than the extension configuration, otherwise the value of the extension configuration cannot be obtained.

Compile and run output:

> Configure project :app
>>>>>>>>  com.yechaoa.plugin.DependenciesPlugin
>>>>>>>>  applicationVariant.getName() = debug

--------------官方库 start--------------
com.google.android.material:material:1.8.0
androidx.appcompat:appcompat:1.5.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10

...(省略部分)
    
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.1
androidx.arch.core:core-runtime:2.1.0
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.1
--------------官方库 end--------------
--------------三方库 start--------------
com.squareup.okhttp3:okhttp:4.10.0
com.squareup.retrofit2:retrofit:2.9.0
com.squareup.okio:okio:3.0.0
com.squareup.okio:okio-jvm:3.0.0
--------------三方库 end--------------

ok, the effect of customizing the plug-in in an independent project, distinguishing all dependencies and printing them out is achieved.

8. Summary

We first introduced the Gradle plug-in, then started with the most basic writing method, then introduced the implementation and usage of the Plugin extension, and finally introduced the process of writing, publishing and external use of Plugin in an independent project with a small example.
Overall, the difficulty is average, but there are some small details that need to be paid attention to, such as mastering the Gradle life cycle, the process of using plug-ins, etc.

9. Finally

Some friends reported that although the previous articles were well written, they were a bit long and not easy to digest. .
Although, Gradle does have a lot of stuff, and it will be streamlined later as appropriate.

Writing is not easy, thank you for your support~

10、GitHub

https://github.com/yechaoa/GradleX

11. Related documents

Guess you like

Origin blog.csdn.net/yechaoa/article/details/133003320