【车载Android】Gradle自定义插件从编写到发布

上一篇文章【车载Android】使用自定义插件实现多语言自动化适配介绍了自定义插件的使用方式,本文则聚焦"造轮子"------详细讲解如何从头开发一个Android Gradle Plugin,以及如何将其发布到Gradle Plugin仓库,实现开源共享。

源码是最好的老师,本文源码:github.com/linxu-link/...

示例的插件源码和脚本均使用kotlin语言。

一、创建插件

在Android工程中编写自定义插件主要有两种方式,分为独立插件私有插件

独立插件、私有插件是为了方便编写文章时进行分类,临时取得名字,官方文档中并没有相关的概念。

1. 独立插件

独立插件一般可以按照以下步骤进行:

(1)创建插件Module

在Android Studio中,点击"File"->"New"->"New Module"。选择"Android Library"作为模块类型,输入插件模块的名称,如"plugin"。

(2)删除模块内容并创建目录

将新建模块中的内容删除,只保留build.gradle文件和src/main目录。在main目录下新建kotlin目录。

(3)配置 buildSrc/build.gradle.kts

scss 复制代码
plugins {
    `java-gradle-plugin` // 核心插件,提供Gradle插件开发支持
    kotlin("jvm") version "1.9.0"
}

repositories {
 maven(url = "https://maven.aliyun.com/repository/central")
    maven(url = "https://maven.aliyun.com/repository/google")
    google()
    mavenCentral()
}

kotlin {
jvmToolchain(17)
}

dependencies {
 implementation(gradleApi())
    implementation(localGroovy())
    implementation(kotlin("stdlib-jdk8"))
} 

更完整代码请参考:github.com/linxu-link/...

(3)创建插件类

java目录下创建包名,如"com.xx.plugin",来新建名为"XXXPlugin"的kotlin文件。让该类实现org.gradle.api.Plugin接口,示例代码如下:

csharp 复制代码
public class MultilingualPlugin implements Plugin<Project> {
    void apply(Project project) {
        System.out.println("========================");
        System.out.println("hello gradle plugin!");
        System.out.println("========================");
    }
}

完整的plugin插件目录如图所示:

2. 私有插件

私有插件一般可以按照以下步骤进行:

(1)创建 buildSrc 目录

在项目根目录下手动创建如下结构(与appmodule同级),其余部分与独立插件完全一致:

bash 复制代码
项目根目录/
├── app/
├── module1/
├── buildSrc/               # 核心目录(Gradle自动识别)
│   ├── src/
│   │   └── main/
│   │       ├── kotlin/     # 推荐用Kotlin(或groovy/、java/)
│   │       └── resources/  # 存放插件描述文件等资源
│   └── build.gradle.kts    # buildSrc的构建配置

(2)配置插件描述文件

main目录下新建resources目录,然后在resources目录里面再新建META-INF目录,接着在META-INF里面新建gradle-plugins目录。最后在gradle-plugins目录里面新建properties文件,如"MultilingualPlugin.properties",并在文件中指明自定义的类。

ini 复制代码
implementation-class=com.wj.plugin.multilingual.MultilingualPlugin
version=0.2.0

完整的plugin插件目录如图所示:

在Android Studio工程中,buildSrc 是一个特殊的目录/模块 ,用于存放项目构建过程中需要用到的自定义代码(如Gradle插件、任务、工具类等)。它是Gradle原生支持的一个"约定目录"------只要在项目根目录下创建名为buildSrc的文件夹,Gradle就会自动将其识别为构建脚本的源码目录,并在构建时优先编译执行其中的代码。

如果你的自定义插件仅用于当前项目(不打算开源或共享给其他项目),buildSrc 是最佳选择(无需发布到仓库,工程内可以直接使用)。

buildSrc除了用于插件开发,还有以下使用场景:

  • 构建工具类:例如封装通用的文件操作、版本号计算、多模块依赖检查等工具方法。

  • 统一配置抽取 :例如将多模块共用的compileSdkVersion、依赖版本号等定义在buildSrc中,实现"一处修改,全局生效"。

3. 私有插件与独立插件的区别

特性 buildSrc 独立插件
适用范围 仅当前项目内部使用 可发布到仓库,供多个项目共享
依赖方式 自动集成,无需手动依赖 需通过implementation添加依赖
发布需求 无需发布,随项目源码一起维护 需发布到Maven/Gradle仓库
适用场景 项目私有构建逻辑 通用插件(如开源插件、公司内部共享插件)

二、Plugin 核心API

在Android Studio中开发Gradle插件时,核心是基于Gradle的API进行扩展。以下是开发中最常用的类、接口和API,我们将逐一讲解。

  • 核心入口Plugin<Project> 接口(apply方法)
  • 项目交互Project 类(任务、配置、文件操作)
  • 用户配置ExtensionContainer + 自定义扩展类
  • 任务定义DefaultTask 类 + 注解(@TaskAction@Input等)
  • 文件操作RegularFilePropertyDirectoryProperty(支持增量构建)
  • 生命周期Gradle 类 + 监听接口
  • 日志输出Logger

1. 核心入口:Plugin<Project>

所有Gradle插件的入口,必须实现此接口,通过apply方法来实现插件的各种逻辑。

kotlin 复制代码
import org.gradle.api.Plugin
import org.gradle.api.Project

// 自定义插件主类
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // 插件逻辑:注册任务、创建扩展配置等
        project.logger.lifecycle("MyPlugin 已应用到项目:${project.name}")
    }
}
  • fun apply(project: Project):插件被应用时调用的核心方法,接收Project实例(代表当前项目)。

2. Project

代表一个Gradle项目(模块或根项目),是插件与项目交互的核心入口,提供任务管理、配置、文件操作等能力。

常用方法:

方法/属性 作用
tasks 获取任务容器(TaskContainer),用于注册/管理任务
extensions 获取扩展容器(ExtensionContainer),用于创建自定义配置
projectDir 当前项目目录(File
buildDir 构建输出目录(File,默认projectDir/build
logger 日志工具(Logger),用于输出日志
dependencies 依赖处理器(DependencyHandler),用于添加依赖
file(path: String) 解析项目相对路径为File对象
afterEvaluate(action: (Project) -> Unit) 项目配置完成后执行的回调

示例:

kotlin 复制代码
override fun apply(project: Project) {
    // 1. 获取项目信息
    println("项目名称:${project.name}")
    println("项目路径:${project.projectDir.absolutePath}")

    // 2. 注册任务
    project.tasks.register("myTask") {
        it.group = "Custom"
        it.description = "自定义任务示例"
        it.doLast {
            println("执行我的任务")
        }
    }

    // 3. 项目配置完成后执行
    project.afterEvaluate {
        println("项目配置已完成,任务数量:${it.tasks.size}")
    }

    // 4. 添加依赖(示例:为"implementation"配置添加依赖)
    project.dependencies.add("implementation", "com.google.guava:guava:31.1-jre")
}

3. 扩展配置(Extension)

扩展配置允许用户在build.gradle.kts中自定义插件参数(如android { ... }配置块)

例如:MultilingualPlugin支持在build.gradle中使用一些自定义配置项目,这些配置项就需要自定义拓展配置。

kotlin 复制代码
multilingual {
    // 启用多语言适配,默认关闭
    enable.set(true)
    // 使用project.rootDir获取项目根目录,再拼接相对路径
    excelFilePath.set(file("${project.rootDir}/language/多语言V1.0.xlsx").absolutePath)
    // 基准语言目录,必须与代码中资源文件目录一致
    baselineDir.set("values")
    // 基准语言编码,必须与Excel文件中的语言编码一致
    defaultLanguage.set("zh-rCN")
}

常用类/方法:

  • ExtensionContainer:通过project.extensions获取,用于创建和管理扩展。

  • create(name: String, type: Class<T>, vararg args: Any):创建扩展配置对象。

示例:定义扩展配置

vbnet 复制代码
open class MultilingualExtension(objectFactory: Project) {
    // 插件是否启用
    val enable: Property<Boolean>

    // Excel文件路径配置
    val excelFilePath: Property<String>

    // 默认语言配置
    val defaultLanguage: Property<String>

    // 基线目录配置
    val baselineDir: Property<String>

    init {
        val objects = objectFactory.objects
        excelFilePath = objects.property(String::class.java).convention("")
        defaultLanguage = objects.property(String::class.java).convention("")
        baselineDir = objects.property(String::class.java).convention("values")
        enable = objects.property(Boolean::class.java).convention(false)
    }
}

然后在Plugin<Project>的实现类中创建配置:

kotlin 复制代码
class MultilingualPlugin : Plugin<Project> {

    override fun apply(project: Project) {
      // 创建根项目级别的扩展配置
      project.extensions.create("multilingual",MultilingualExtension::class.java)
    }
}

然后用户就可以在build.gradle.kts中使用multilingual进行配置。

4.任务(Task)相关类

任务是Gradle执行的最小单元,自定义任务需继承DefaultTask,并通过注解标记输入输出和执行逻辑。

  1. DefaultTask

自定义任务的基类,提供任务基础能力。

  1. 核心注解:
  • @TaskAction:标记任务的执行方法(任务被调用时执行)。
  • @Input:标记输入参数(用于增量构建,输入变化时任务会重新执行)。
  • @OutputFile/@OutputDirectory:标记输出文件/目录(用于增量构建,输出变化时任务会重新执行)。
  • @Optional:标记参数为可选(可不为其赋值)。
  1. TaskContainer 接口

通过project.tasks获取,用于注册、查询、管理任务。

常用方法:

  • register(name: String, type: Class<T>, configuration: T.() -> Unit):注册任务(推荐,支持延迟配置)。
  • named(name: String):根据名称获取已注册的任务(返回Provider<Task>)。
  • findByName(name: String):根据名称查找任务(返回Task?)。

示例:自定义任务

less 复制代码
open class MultilingualTask : DefaultTask() {
    @get:Input // excel 文件路径
    val excelFilePath: Property<String> = project.objects.property(String::class.java)

    @get:Input // 默认语言
    val defaultLanguage: Property<String> = project.objects.property(String::class.java)

    @get:Input // 基线文件
    val baselineDir: Property<String> = project.objects.property(String::class.java)

    @TaskAction
    fun generateTranslations() {
    
    }
}

然后在Plugin<Project>的实现类中进行注册:

kotlin 复制代码
class MultilingualPlugin : Plugin<Project> {

    override fun apply(project: Project) {
         // ....省略其它方法。
         
        // 注册生成翻译的任务
        project.tasks.register("generateTranslations", MultilingualTask::class.java){             task ->
            // 从根配置获取参数
            task.excelFilePath.set(rootExtension.excelFilePath)
            task.defaultLanguage.set(rootExtension.defaultLanguage)
            task.baselineDir.set(rootExtension.baselineDir)
        }
      
    }
}

5. 文件操作相关API

Gradle提供了类型安全的文件操作API,支持增量构建和路径管理,优于直接使用java.io.File

常用类:

  • RegularFileProperty:用于表示单个文件(支持增量构建,推荐替代File)。
  • DirectoryProperty:用于表示目录(支持增量构建)。
  • ProjectLayout:通过project.layout获取,提供项目目录的类型安全访问。

示例:

less 复制代码
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty

abstract class FileProcessorTask : DefaultTask() {
    // 输入目录(通过ProjectLayout获取)
    @get:InputDirectory
    abstract val inputDir: DirectoryProperty

    // 输出文件
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun process() {
        val files = inputDir.get().asFile.listFiles()?.toList() ?: emptyList()
        val content = "目录下文件数量:${files.size}\n文件列表:${files.joinToString { it.name }}"
        outputFile.get().asFile.writeText(content)
    }
}

// 在插件中配置路径
override fun apply(project: Project) {
    project.tasks.register<FileProcessorTask>("processFiles") {
        // 使用ProjectLayout获取标准目录(如src/main/resources)
        it.inputDir.set(project.layout.projectDirectory.dir("src/main/resources"))
        // 输出到build目录下的processed.txt
        it.outputFile.set(project.layout.buildDirectory.file("processed.txt"))
    }
}

6.依赖管理相关类

通过依赖管理API可动态添加、修改项目依赖。

常用类:

  • DependencyHandler:通过project.dependencies获取,用于添加依赖。
  • Configuration:依赖配置(如implementationtestImplementation),通过project.configurations获取。

示例:动态添加依赖

kotlin 复制代码
override fun apply(project: Project) {
    // 为"implementation"配置添加依赖
    project.dependencies.add("implementation", "androidx.core:core-ktx:1.12.0")

    // 为"testImplementation"添加测试依赖
    project.dependencies.add("testImplementation", "junit:junit:4.13.2")

    // 动态创建自定义配置并添加依赖
    val customConfig = project.configurations.create("customLibs")
    project.dependencies.add("customLibs", "com.squareup.okhttp3:okhttp:4.11.0")
}

7. 生命周期监听相关类

通过Gradle生命周期API可在构建的特定阶段插入逻辑(如构建开始/结束、任务执行前后)。

常用类:

  • Gradle:通过project.gradle获取,代表整个构建过程。
  • TaskExecutionListener:监听任务执行事件。

示例:监听构建生命周期

kotlin 复制代码
override fun apply(project: Project) {
    // 1. 构建开始前执行
    project.gradle.buildStarted {
        project.logger.lifecycle("构建开始!")
    }

    // 2. 所有任务执行完成后执行
    project.gradle.buildFinished { result ->
        project.logger.lifecycle("构建结束!结果:${if (result.success) "成功" else "失败"}")
    }

    // 3. 监听单个任务的执行
    project.tasks.named("generateReport").configure { task ->
        task.doFirst {
            logger.lifecycle("generateReport 任务开始执行...")
        }
        task.doLast {
            logger.lifecycle("generateReport 任务执行完成!")
        }
    }
}

8. 日志输出:Logger

用于输出构建日志,支持不同级别(比println更规范,可控制日志可见性)。

常用方法:

  • lifecycle(message: String):生命周期级日志(默认可见)。
  • info(message: String):信息级日志(需--info参数可见)。
  • debug(message: String):调试级日志(需--debug参数可见)。
  • warn(message: String):警告级日志。
  • error(message: String):错误级日志。

示例:

kotlin 复制代码
override fun apply(project: Project) {
    project.logger.lifecycle("===== 生命周期日志 =====")
    project.logger.warn("这是一个警告日志")
    project.logger.debug("这是调试日志(需--debug可见)")
}

三、上传 Plugin

将编写好的Gradle插件上传到 plugins.gradle.org(Gradle官方插件仓库),需要完成配置插件的基础信息、准备发布环境、执行发布命令等步骤。

1. 准备工作

(1)注册Gradle账号

访问 Gradle Plugin Portal,使用GitHub账号登录(推荐),完成账号验证。

(2)获取发布密钥

登录后,进入 个人设置页面,生成 API keyAPI secret(用于发布认证,仅显示一次,需保存)。

(3)确保插件符合规范

  • 插件ID需唯一(格式:[命名空间].[用户名].[插件名],如 io.github.username.pluginname)。

  • 必须包含文档(至少有 README.md 说明使用方法)。

  • 源码需开源(如托管在GitHub)。

2. 配置插件项目

在插件模块的 build.gradle中添加发布配置

csharp 复制代码
plugins {
    `java-gradle-plugin`// 核心插件,提供插件开发支持
    `maven-publish`// 支持Maven格式发布
    id("com.gradle.plugin-publish") version "1.3.1" // 发布到Gradle Plugin Portal的专用插件
    kotlin("jvm") // 若用Kotlin编写插件
}
// 1. 插件元信息(必填)
group = "io.github.linxu-link" // 命名空间,直接用GitHub用户名
version = "0.2.0"
val pluginDescription = "A plugin that automatically generates Android multi-language resources based on Excel." // 插件描述
// 2. 插件核心配置
gradlePlugin {
    website.set("https://github.com/linxu-link/MultilingualPlugin") // 插件主页(GitHub仓库地址)
    vcsUrl.set("https://github.com/linxu-link/MultilingualPlugin") // 版本控制地址
    plugins {
        register("multilingualPlugin") { // 插件名称(自定义)
            id = "io.github.linxu-link.multilingual" // 插件唯一ID(必须全仓库唯一)
            implementationClass = "com.wj.plugin.multilingual.MultilingualPlugin" // 插件主类
            displayName = "Android Multilingual Plugin"
            description = pluginDescription
            tags.set(listOf("android", "translation", "excel", "localization")) // 用于搜索的标签
        }
    }
}

// 配置 Maven 格式的发布信息
publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])
            groupId = project.group.toString()
            artifactId = "multilingual-plugin"
            version = project.version.toString()
        }
    }
}

// 生成源码和文档JAR(提升可用性,推荐)
java {
    withSourcesJar() // 源码JAR(方便用户调试)
    withJavadocJar() // 文档JAR(生成API文档)
}

kotlin {
    jvmToolchain(17)
}
// 依赖配置(根据插件功能添加)
dependencies {
    implementation(gradleApi()) // Gradle API依赖
    implementation(localGroovy())
    implementation(kotlin("stdlib-jdk8")) // kotlin 依赖
}

3.执行发布命令

(1)配置密钥

在项目根目录的 gradle.properties 中添加发布密钥(一定不要提交到代码仓库):

ini 复制代码
gradle.publish.key=你的API key
gradle.publish.secret=你的API secret

(2)清理并发布

在终端执行以下命令:

bash 复制代码
# 清理构建缓存
./gradlew clean

# 发布插件
./gradlew publishPlugins

4. 验证发布

首次发布需人工审核(1-2个工作日),审核通过后,会收到Gradle Plugin官方发送的邮件。后续版本更新会自动审核。

然后在plugins.gradle.org可以看到插件详情中已经给出了集成的方式。

5.常见问题

(1)命名空间不一致被驳回

确保 group 与插件 id 的前缀一致(如 group=io.github.yourusernameid=io.github.yourusername.myplugin),并且username要和github上的username保持一致。

(2)缺失文档被驳回

需在GitHub仓库根目录添加 README.md,详细说明插件功能、集成步骤和使用示例。

相关推荐
阿巴斯甜17 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker17 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952718 小时前
Andorid Google 登录接入文档
android
黄林晴19 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android