Gradle for Android-创建task和plugin

迄今为止,我们已经为gradle build手动编辑了属性和学习了如何运行task。本章节中,我们将会对这些属性进行深度理解,并开始创建我们自己的task。一旦我们知道如何书写自己的task,便可更进一步,了解如何制作我们自己的可以在几个项目中重用的plugin。

在了解如何创建自定义task前,需要学习一些重要的Groovy概念。因为对Goovy如何工作有个基本的理解使得自定义task和plugin更加容易。了解Groovy也帮助理解gradle是如何工作的以及build配置文件是如何发挥作用。

本章节,我们将了解以下主题:

  • 理解Groovy
  • task入门
  • 连接到Android plugin
  • 创建自己的plugin

理解Groovy

大部分的Android开发者也是熟练的Java开发者,了解Groovy较之于Java如何工作是非常有趣的。如果你是一位Java开发者的话阅读Groovy是很容易的,但是写Groovy代码就会是很难的任务了如果没有简短的介绍的话。

介绍

Groovy基于Java而生并运行于Java虚拟机上。它的目标就是成为一门更简单、更直接的既可作为脚本语言也可作为成熟的编程语言使用的语言。贯穿本节,我们将会比较Groovy和Java使得更易把握Groovy如何工作以及清晰地看到两种语言间的差异。

在Java中,打印一个字符串到屏幕上代码如下:

System.out.println("Hello, world!");

在Groovy中,使用如下代码可实现相同功能:

println 'Hello, world!'

可以立刻注意到几个差异之处:

  • 没有System.out命名空间
  • 方法参数周围没有圆括号
  • 句末没有分号

这个例子在字符串周围使用了单引号。对于字符串既可使用单引号也能使用双引号,但是它们有不同的用处。双引号也能包括内插值表达式。插值法是评估包含占位符的字符串,并用它们的值替换这些占位符。这些占位符表达式可以是变量或方法。包含方法或多个变量的占位符表达式需要用大括号包围并添加前缀。包含单个变量的占位符表达式可以只添加前缀$就可以了。这有一些Groovy中内插值字符串的例子:

def name = 'Andy'
def greeting = "Hello, $name!"
def name_size "Your name is ${name.size()} characters long."

greeting变量包括字符串“Hello ,Andy”,name_size是“Your name is 4 character long”。

字符串插值法也允许你动态执行代码。例子就是打印当前数据的正当的代码:

def method = 'toString'
new Date()."$method"()

在Java中使用看起来非常陌生,但是在动态编程语言中是正常的语法和行为。

类和成员

在Groovy中创建一个类和在Java中很相像。这里是个简单的包含一个成员的类:

class MyGroovyClass {
    String greeting
    String getGreeting() {
        return 'Hello!'
    }
}

注意类和成员都没有明确的访问修饰符。Groovy中默认的访问修饰符与Java不同。类自身都是公共的,就像方法一样,但是类成员都是私有的。

使用MyGroovyClass创建一个新的实例:

def instance = new MyGroovyClass()
instance.setGreeting 'Hello, Groovy!'
instance.getGreeting()

可以使用关键词def定义新的变量。一旦有了新的类的实例,可以操作它的成员。访问符被Groovy自动添加。你仍然可以重写它们,就如我们在MyGroovyClass的定义里的getGreeting()做的一样。如无明确说明,仍然可以对类中的每个成员使用getter和setter方法

如果尝试直接调用成员,事实上会调用getter方法。这意味着不需要输出instance.getGreeting
(),可以使用更简短的instance.greeting代替:

println instance.getGreeting()
println instance.greeting

两行代码打印的是相同的内容。

方法

与变量类似,不需要为方法定义特定的返回值。无论如何你可以自由行事,即使是为了清晰性。另Java和Groovy方法的另一个差异之处在于Groovy中,方法的最后一行默认被返回,即使没有使用return关键词。

为了演示Java和Groovy的差异,思考一下这个Java返回平方数方法的例子:

public int square(int num) {
    return num * num;
}
square(2);

需要指明方法是公共可访问的、返回类型以及参数类型。在方法最后,需要返回一个返回类型值。

Groovy中相同的方法定义如下所观:

def square(def num) {
    num * num
}
square 4

返回类型和参数类型都没有明确定义。使用了def关键词代替明确类型,而且方法没有使用return关键词模糊的返回了一个值。然而,为了清晰性还是建议使用return关键词。当你调用方法时,不需要圆括号或分号。

这也是Groovy中另一种甚至更简短的定义新方法的方式。相同的square方法如是下观:

def square = { num ->
    num * num
}
square 8

这不是常规的方法,而是闭包。闭包的概念在Java中不存在,但是在Groovy和Gradle中扮演着重要的角色。

闭包

闭包是可以接受参数和返回一个值的匿名代码块。它们可被分配给变量和作为参数传给方法。

可以通过在大括号间添加代码块简单地定义一个闭包,正如之前的例子所看到的。如果想要更明确,可以添加定义类型,如下:

Closure square = {
    it * it
}
square 16

添加Closure类型使得每个使用该代码的人都很清晰定义了一个闭包。之前的例子也介绍了模糊无类型参数it的概念。如果没有明确添加参数到闭包中,Groovy将自动添加一个。该参数总是被称作it,而且可以在所有闭包中使用它。如果调用者没有指定任何参数,it就是null或empty。这样可以使得代码更简明,但是只有在闭包只有一个参数时才有用。

在Gradle环境下,我们一直使用闭包。本书中,目前为止,我们已经把闭包视为块。这意味着,比如android块和dependencies块都是闭包。

集合

在Gradle环境下使用Groovy有两个重要的集合概念:list和map。

在Groovy中创建一个新的list很容易。不需要特殊的初始化,可以如下简单地创建一个list:

List list = [1, 2, 3, 4, 5]

迭代list也是非常容易的。可以使用each方法迭代list中的每个元素:

list.each() { element ->
    println element
}

each方法使得你能够访问list中的每个元素。可以使用之前提到的it变量使得代码更简短:

list.each() {
    println it
}

Gradle环境下另一个重要的集合概念就是Map。Map在几个Gradle settings和方法中被使用到。map简而言之就是包含了一个键值对的list。可以如下定义一个map:

Map pizzaPrices = [margherita:10, pepperoni:12]

为了访问map中指定的项,使用get方法或中括号。

pizzaPrices.get('pepperoni')
pizzaPrices['pepperoni']

Groovy针对这个功能也有捷径。可以针对map元素使用点记法,使用关键词索引值:

pizzaPrices.get('pepperoni')
pizzaPrices['pepperoni']

Gradle中的Groovy

既然已经了解了Groovy基础,掉回头学习一个Gradle build文件并阅读是个有意思的体验。注意理解为什么配置语法看起来如它的样子变得更加容易了。例如,看看Android plugin被应用到build的这行代码:

apply plugin: 'com.android.application'

这个代码块是充满了Groovy快捷法的。不过不使用任何快捷方式书写,如下所观:

project.apply([plugin: 'com.android.application'])

不使用Groovy快捷方式重写这行代码使得apply()是Project类的一个方法更加清晰,这是每个Gradle build的基础构建块。apply()方法带有一个参数,该参数是带有key为plugin,值为com.android.application的Map。

另一个例子就是dependecies块。以前我们定义的dependencies如下:

dependencies {
    compile 'com.google.code.gson:gson:2.3'
}

我们现在知道这个块是个闭包,被传到一个Project对象的dependencies方法中。这个闭包被传到一个DependencyHandler中,它包含了add()方法。该方法接收三个参数:一个字符串定义配置,一个对象定义依赖符和一个包含了该依赖特定属性的闭包。全写出来如下:

project.dependencies({
    add('compile', 'com.google.code.gson:gson:2.3', {
    // Configuration statements
    })
})

应该开始对迄今为止我们已经看到的build配置文件有更多的理解,既然你已经知道了它背后的原理。

如果想在底层了解更多Gradle利用Groovy的方式,可以使用官方文档http://gradle.org/docs/current/javadoc/org/gradle/api/Project.html

task入门

自定义Gradle task可以极大的提升开发者的日常生活。task可以操作已存在的build进程、添加新的build步骤或影响build的输出。可以实现简单的task,例如通过勾入Gradle的Android plugin重命名一个APK。task使得你能够运行更复杂的代码,可以为几种分辨率生成不同的图片。一旦知道如何创建自己的task,你会发现自己被允许改变build进程的每个方面。这尤其如此当你学习如何挂钩Anroid plugin时。

定义task

Task属于Project对象,每个task都实现了Task接口。定义一个新的task的最容易的方式就是用task名称作为参数执行task方法。

task hello

这定义了task,但执行时不会做任何事。为了创建一个多少有点用的task,需要添加一些action。一个常见的开发者错误就是如下创建task:

task hello {
    println 'Hello, world!'
}

当执行这个task,输出如下:

$ gradlew hello
Hello, world!
:hello

从输出可能得到这个task的结果,但事实上,“Hello world”甚至可能在task被执行前打印。为了理解发生了什么,我们需要重返基础。在第一章,我们讨论了Gradle构建的生命周期。在任何Gradle构建中都有三个阶段:初始化阶段、配置阶段和执行阶段。当以如上面例子相同的方式添加代码到task中,实际上建立了task的配置。即使你要执行一个不同的task,“Hello World”消息还是会显示出来。

如果想在执行阶段添加action到task中,使用这个符号:

task hello << {
    println 'Hello, world!'
}

唯一的差别在于<<在闭包前面。告诉Gradle这块代码是针对执行阶段而非配置阶段的。

为了延时差异,请看如下build文件:

task hello << {
    println 'Execution'
}
hello {
    println 'Configuration'
}

我们定义了task hello,当被执行时会打印到屏幕上。我们也为hello task的配置阶段定义了代码,会打印Configuration到屏幕上。虽然配置块在实际的task代码定义之后被定义,但仍会率先执行。以上例子输出如下:

$ gradlew hello
Configuration
:hello
Execution

因为Groovy有许多快捷方式,在Gradle中有好几种定义task的方式:

task(hello) << {
    println 'Hello, world!'
}
task('hello') << {
    println 'Hello, world!'
}
tasks.create(name: 'hello') << {
    println 'Hello, world!'
}

前两个块是使用Groovy实现同一事物的两种不同方式。可以使用圆括号,但是不需要。也不需要在参数周围有单引号。这两个块中,我们调用task()方法,有两个参数:一个标识task名称的字符串和一个闭包。task()方法是Gradle的Project类的一部分。

最后一个块没有使用task()方法。代之,利用了一个名为tasks的对象,它是一个TaskContainer实例,而且在每个Project对象中都有。这个类提供了一个create()方法使用一个Map和一个闭包作为参数并返回一个Task。

写成简短形式很方便而且大部分在线的例子和教程都会使用它们。然而,写成更长的形式在学习的时候非常有用。按照这种方式,Gradle更少变化,而且更容易理解发生了什么。

task剖析

Task接口是所有task的基础而且定义了许多属性和方法。这些都被一个叫做DefaultTask的类实现。这是标准的task类型实现,而且当创建一个新的task时,也是基于DefaultTask的。

从技术上讲,DefaultTask并非是真的实现了Task接口中所有方法的类。Gradle有个内部类叫做AbstractTask,包含了Task接口的所有方法的实现。但因为AbStractTask是内部的,我们无法重写。因此,我们集中于DefaultTask,它是继承AbstarctTask的,而且我们能够重写。

每个Task都包含了许多Action对象。当一个task被执行时,所有的action都会依次被执行。为了添加action到task中,可以使用doFirst()和doLast()方法。这些方法都采用了闭包作为参数,而且将之传到Action对象中。

你总是需要使用doFirst()或doLast()添加代码到task中如果想要该代码成为执行阶段的一部分。我们之前用来定义task的左移操作符<<是doFirst()方法的快捷方式。

这有一个使用doFirst()和doLast()方法的例子:

task hello {
    println 'Configuration'
    doLast {
        println 'Goodbye'
    }
    doFirst {
        println 'Hello'
    }
}

当执行hello task,输出如下:

$ gradlew hello
Configuration
:hello
Hello
Goodbye

虽然打印“Goodbye”的这行代码先于打印“Hello”的那行代码被定义,但是当task被执行时都是以正确的顺序结束。你甚至可以多次使用doFirst()和doLast(),如下:

task mindTheOrder {
    doFirst {
        println 'Not really first.'
    }
    doFirst {
        println 'First!'
    }
    doLast {
        println 'Not really last.'
    }
    doLast {
        println 'Last!'
    }
}

执行该task会返回以下输出:

$ gradlew mindTheOrder
:mindTheOrder
First!
Not really first.
Not really last.
Last!

注意doFirst()总是添加一个action到task的起点,doLast()总是添加一个action到终点。这意味着你当使用这些方法时需要注意,尤其当顺序很重要时。

说到有序task,可以使用mustRunAfter方法。该方法允许你去影响gradle如何构造依赖图。当使用mustRunAfter时,如果有个task被执行,你指定了一个必须先于另外一个被执行。

task task1 << {
    println 'task1'
}
task task2 << {
    println 'task2'
}
task2.mustRunAfter task1

同时运行task1和task2将总是导致task1先于task2执行,而不管你指定的顺序:

$ gradlew task2 task1
:task1
task1
:task2
task2

mustRunAfter方法并不在task间添加依赖;仍然有可能执行task2而不执行task1.如果需要一个task依赖另一个,使用dependsOn()方法代替。mustRunAfter和dependsOn()的差异用个例子解释最好:

task task1 << {
    println 'task1'
}
task task2 << {
    println 'task2'
}
task2.dependsOn task1

当执行task2,而没执行task1时如下所观:

$ gradlew task2
:task1
task1
:task2
task2

使用mustRunAfter(),当同时运行二者时,task1总是先于task2执行,但二者可被独立执行。使用dependsOn(),task2的执行总是触发task1,虽然没有明确提及。这是一个重要的差异。

使用task简化发布过程

发布一个Anroid app到Google Play store之前,需要使用证书对之签名。为了实现签名,需要创建自己的keystore,它包含了一套私钥。应用有了自己的keystore和私钥后,可在gradle中定义签名配置,如下:

android {
    signingConfigs {
        release {
            storeFile file("release.keystore")
            storePassword "password"
            keyAlias "ReleaseKey"
            keyPassword "password"
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

这个方法的缺点是你的keystore密码在仓库中是以明文形式存储的。如果你致力于一个开源项目,这是肯定不行的。每个拥有keystore文件和密码的人都能使用你的身份发布app。为了避免如此,可以创建一个task,每次打包发布安装包时询问发布密码。虽然有点笨,而且使得你的build服务器无法自动生成发布build。一个存储keystore密码的好的解决方案就是创建一个不包含在仓库中的配置文件。

通过在项目根目录下创建一个private.properties文件开始,并添加如下代码:

release.password = thepassword

我们假设keystore和key的密码都是相同的。如果有两个不同的密码,很容易添加一个第二属性。

一旦完成之后,可以定义一个名为getReleasePassword的新的task:

task getReleasePassword << {
    def password = ''
    if (rootProject.file('private.properties').exists()) {
        Properties properties = new Properties();
        properties.load( rootProject.file('private.properties').newDataInputStream())
        password = properties.getProperty('release.password')
    }
}

这个task将会在项目的根目录下寻找名为private.properties的文件。如果文件存在,task将会从其内容中加载所有属性。properties.load()方法寻找键值对,例如我们在属性文件中定义的release.password。

为了确保所有人都能不用私有属性文件运行脚本,或者处理属性文件存储位置,但密码属性没有出现,添加一个回调。如果密码仍然是空的,在控制台询问密码:

if (!password?.trim()) {
    password = new String(System.console().readPassword
("\nWhat's the secret password? "))
}

使用Groovy检测一个字符串是否为空是非常简单的事。password?.trim()中的问号做了空检查而且将会阻止trim()方法调用如果password是空的话。我们不需要明确的检查null或empty,因为null和empty字符串都等于false在if语句环境下。

new String()是必须的因为System.readPassword返回一个字节数组,需要被转换成字符串。

一旦有了keystore密码,可以为release build配置签名配置:

android.signingConfigs.release.storePassword = password
android.signingConfigs.release.keyPassword = password

既然已经完成了task,需要确认当执行一个release build时它是被执行了的。为了实现,在buidl.gradle中添加如下:

tasks.whenTaskAdded { theTask ->
    if (theTask.name.equals("packageRelease")) {
            theTask.dependsOn "getReleasePassword"
}
}

当task被添加到依赖图时,通过添加一个需要被运行的闭包挂钩到Gradle和Android plugin。密码不被要求知道packageRelease task被执行,所以我们确保packageRelease依赖于getReleasePassword task。不能仅使用packageRelease.dependsOn()的原因是Gradle Android plugin基于build变体,动态生成打包task。这意味着这packageRelease task不存在直到Android plugin发现了所有的build变体。发现的过程始于每个单独构建之前。

添加task并构建挂钩之后,执行gradlew assembleRelease的结果如下:

Gradle for Android-创建task和plugin_第1张图片

正如在过程截图看到的,private.properties文件不可用,所以task在控制台中询问密码。这种情况下,我们也添加了一条消息解释如何创建属性文件和添加密码属性使得以后的build更加容易。一旦我们的task选择了keystore密码,gradle就能够打包app并完成build。

为了使得task工作,勾入gradle和Android plugin是必不可少的。这是个非常强大的概念,所以我们会详细探索。

连接到Android Plugin

Android开发时,我们想要改变的大部分task都是与Android plugin相关的。扩展task的行为是可能的通过勾入build进程。在之前的例子中,我们已经看到如何在自定义的task上添加依赖和在常规的build进程中添加一个新的task。在本段落,我们将会了解一些Android特定的build 挂钩的可能性。

勾入到Android plugin的一种方式就是修改build变体。这种方式非常直接;只需要把以下代码迭代到app所有的build变体中:

android.applicationVariants.all { variant ->
    // Do something
}

为了获得build变体集合,可以使用applicationVariants对象。一旦引用一个build变体,可以访问并修改它的属性,例如名称等等。如果你想对同一个Android library使用相同的逻辑,使用libraryVariants代替applicationVariants。

注意我们使用all()方法代替我们之前提到each()方法迭代build变体。这是必须的因为each()是在build变体被Android plugin创建之前的评估阶段被触发。all()方法是每当一个新的项被添加到集合中时就会触发。

挂钩可用于改变APK的名称在它被保存之前,添加版本号到文件名中。这使得不用手动编辑文件名称,也很容易去维护APK的存档。在下一段落,我们将会看到如何实现。

自动重命名APK

操纵build进程的一个常用案例就是重命名APK名称包含版本号当它们被打包之后。可以通过迭代app的build变体和改变它的输出的outputFile属性实现,如下面代码:

android.applicationVariants.all { variant ->
    variant.outputs.each { output ->
        def file = output.outputFile
        output.outputFile = new File(file.parent,file.name.replace(".apk", "-${variant.versionName}.apk"))
    }
}

每个build变体都有一个输出集合。Android app的输出就是一个APK。输出对象都有一个类型属性文件叫做outputFile。一旦知道输出路径,可以改变它。这个例子中,我们添加变体的版本名称到文件名称中。这将定义一个APK被命名成app-debug-1.0.apk代替app-debug.apk.

使用gradle task的简化版,为Android plugin组合build挂钩的能力打开了一个充满可能性的时间。在下一段落中,我们将会看到如何为app的每个build变体创建一个task。

动态创建新task

因为gradle工作和task被构造的方式,我们可以容易地在配置阶段基于Android build变体创建自己的task。为了示范这个强大的概念,我们将会创建一个task不仅安装而且运行app的任意的build变体。install task是Android plugin的一部分,但是如果你从命令行接口中使用installDebug task安装app,将仍然需要手动启动它当安装结束时。我们将在本节创建的task将会将会消除最后一步。

通过勾入我们之前使用过的applicationVariants属性开始:

android.applicationVariants.all { variant ->
    if (variant.install) {
        tasks.create(name: "run${variant.name.capitalize()}",
            dependsOn: variant.install) {
                description "Installs the ${variant.description} and runs the main launcher activity."
        }
    }
}

对于每个变体,我们检查它是否有个有效的install task。它必须要有因为我们将要创建的run task将依赖这个install task,而且基于变体名称对之命名。我们也会使得新的task依赖variant.install。这将触发install task在我们的task被执行之前。在tasks.create()闭包内部,通过添加一个描述开始,它将被展示当执行gradlew tasks时。

除了添加描述之外,我们也需要添加task action。在此例中,我们想要导入app。可以在一个已连接的设备或模拟器上导入一个app使用Android Debug Tool(ADB)工具:

$ adb shell am start -n com.package.name/com.package.name.Activity

Gradle有个方法叫做exec()使得执行一个命令行进程成为可能。为了使得exec()工作,我们需要提供一个可执行的出现在PATH中的环境变量。我们也需要使用args属性传入所有参数,它带有一串字符串,如下:

doFirst {
    exec {
        executable = 'adb'
        args = ['shell', 'am', 'start', '-n',
"${variant.applicationId}/.MainActivity"]
    }
}

为了得到全包名,使用build变体的application ID,它包括一个后缀,如果被提供的话。但是使用后缀有个问题。虽然添加了后缀,activity的类路径仍然相同。例如,思考如下配置:

android {
    defaultConfig {
        applicationId 'com.gradleforandroid'
    }
    buildTypes {
        debug {
            applicationIdSuffix '.debug'
        }
    }

包名是com.gradlefroandroid.debug,但activity的路径仍然是com.gradleforandroid.Activity。为了确保得到activity的正确的类,从application ID中除去后缀:

doFirst {
    def classpath = variant.applicationId
    if(variant.buildType.applicationIdSuffix) {
        classpath -= "${variant.buildType.applicationIdSuffix}"
    }
    def launchClass = "${variant.applicationId}/${classpath}.MainActivity"
    exec {
        executable = 'adb'
        args = ['shell', 'am', 'start', '-n', launchClass]
    }
}

首先,基于application ID,创建一个名为classpath的变量。然后发现被buildType.applicationIdSuffix属性提供的后缀。在Groovy中,使用减号操作符从一个字符串中减去另一个字符串是可能的。这些改变确保了安装之后运行app不会失败当后缀被使用的时候。

创建自己的plugin

如果有个你想要在好几个项目中重用的Gradle task集合,把这些task提取到一个自定义的plugin中是有意义的。这使得重用你自己的build逻辑和与他人共享该逻辑都是可能的。

plugin可使用Groovy重写,也可用其他使用了JVM的语言,例如Java和Scala。事实上,大部分的Gradle Android plugin都是用Java与Groovy混合编写的。

创建一个简单的plugin

为了提取已存储于你的build配置文件中的build逻辑,可以在build.gradle文件中创建一个plugin。这是自定义plugin最容易开始的方式。

为了创建一个plugin,创建一个新的实现了Plugin接口的类。我们将使用本章里之前写的代码,它动态创建一个run task。我们的plugin如下所观:

class RunPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.android.applicationVariants.all { variant ->
            if (variant.install) {
                project.tasks.create(name:"run${variant.name.capitalize()}",
                dependsOn: variant.install) {
            // Task definition
                }
            }
        }
    }
}

Plugin接口定义了一个apply()方法。当plugin在build文件中被使用时,gradle会调用这个方法。项目被做传递为参数传递以便plugin可以配置项目或使用它的方法和属性。在前面的例子中,我们不能直接从Android plugin中调用属性。代之,我们首先需要访问项目对象。记住这要求Android plugin要在我们自定义的plugin被应用之前被应用到项目中。否则,project.android将会导致问题。

task的代码与之前相同,除了一个方法调用外:代之调用exec(),我们现在需要调用project.exec().

为了确保plugin被应用到我们的build配置中,添加以下代码到build.gradle中:

apply plugin: RunPlugin

分发plugin

为了发布一个plugin并与他人共享,需要把它移到一个独立模块(或项目)中。一个独立的plugin有其自己的build文件去配置依赖和分发方式。这个模块生成一个JAR文件,包含了plugin类和属性。你可以使用这个JAR文件在几个模块和项目中应用该plugin,并与他人共享。

正如任意Gradle项目一样,创建一个build.gradle文件配置build:

apply plugin: 'groovy'
dependencies {
    compile gradleApi()
    compile localGroovy()
}

既然用Groovy编写了plugin,我们需要应用该Groovy plugin。Groovy plugin继承了Java plugin,而且使得我们能够构建和打包Groovy类。Groovy和简单的Java都是被支持的,所以我们可以混合它们如果你喜欢的话。你甚至可以使用Groovy继承一个Java类,或其他方式。这使得它很容易入门,虽然你没有信心使用Groovy对于任何事物。

我们的build配置文件包含两个依赖:gradleApi()和localGroovy()。Gradle API被要求从我们自定义的plugin中访问Gradle命名空间,而且localGroovy()是伴随Gradle安装的Groovy SDK的一个分发。为了便利Gradle默认提供这些依赖。如果gradle没有提供这些立即可用的依赖,我们要手动下载和引用它们。

如果计划公开地分发plugin,确认要指定group和version信息在build配置文件中,如下:

group = 'com.gradleforandroid'
version = '1.0'

为了从在我们独立模块中的代码开始,首先需要确认使用正确的目录结构:

plugin
└── src
        └── main
                ├── groovy
                │        └── com
                │               └── package
                │                      └── name
                └── resources
                        └── META-INF
                                └── gradle-plugins

正如其他Gradle模块一样,我们需要提供一个src/main目录。因为这是一个Groovy项目,main的子目录是叫做groovy替代java。另一个main的子目录叫做resources,我们用它指定我们的plugin属性。

我们在包目录下创建一个叫做RunPlugin.groovy的文件,在其中我们为plugin定义了类:

package com.gradleforandroid
import org.gradle.api.Project
import org.gradle.api.Plugin
class RunPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.android.applicationVariants.all { variant ->
// Task code
        }
    }
}

为了Gradle能够发现plugin,我们需要提供一个属性文件。添加属性文件到src/main/resources/META-INF/gradle-plugins/目录下。文件名称需要匹配我们的plugin的ID。对于RunPlugin,文件被命名为com.gradleforandroid.run.properties,而且内容如下:

implementation-class=com.gradleforandroid.RunPlugin

属性文件唯一包含的就是包和实现了Plugin接口的类的名称。

当plugin和属性文件具备时,可以使用gradlew assemble命令构建plugin。这条命令在build输出目录创建了一个JAR文件。如果想要把plugin推送到Maven仓库,需要应用应用Maven plugin:

apply plugin: 'maven'

接下来,需要配置uploadArchives task,如下:

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: uri('repository_url'))
        }
    }
}

uploadArchives task是一个预定义的task。一旦在这个task中配置了一个仓库,可以执行它去发布你的plugin。本书中我们不会讨论如何建立Maven仓库。

如果你想要使得plugin公开可用,考虑一下把它发布到Gradleware’s plugin portal(https://plugins.gradle.org/)。这个plugin portal有大量的gradle plugin(不仅仅针对于Android开发)而且也是你要继承Gradle的默认行为的地方。可以发现如何发布一个plugin的信息在文档https://plugins.gradle.org/docs/submit中。

本书并不包含为plugin写测试,但是如果你计划使得你的plugin公开可用的话,非常建议你这么做。在gradle用户指南中可以发现更多为plugin写测试的信息https://gradle.org/docs/current/userguide/custom_plugins.html#N16CE1。

使用自定义的plugin

为了使用一个plugin,需要把它添加到buildscript块中作为一个依赖。首先,需要配置一个新的仓库。仓库的配置依赖于plugin被分发的方式。其次,需要配置plugin的类路径在dependencies块中。

如果想要添加我们之前例子中创建的JAR文件,可以定义一个flatDir仓库:

buildscript {
    repositories {
        flatDir { dirs 'build_libs' }
    }
    dependencies {
        classpath 'com.gradleforandroid:plugin'
    }
}

如果我们想把plugin上传到了Maven或Ivy仓库,这将会有点不同。我们已经在第三章学习过了依赖管理,所以我们不会重复不同的选项。

建立依赖之后,我们需要应用这个plugin:

apply plugin: com.gradleforandroid.RunPlugin

使用apply()方法时,gradle创建一个plugin类的实例,并执行这个plugin自己的apply()方法。

总结

本章节中,我们发现了Groovy是如何不同于Java以及Groovy是如何在Gradle中使用的。我们看来的如何创建自己的task和如何挂钩到Android plugin中,后者给了我们很大的权利操作build进程或动态添加我们自己的task。

本章最后一部分,我们了解了创建plugin和确保我们可以在几个项目中重用它们通过创建一个独立的plugin。关于plugin还有更多需要了解,但不幸地是,我们不能在本书中全部了解。幸运地是,gradle用户指南有完整的描述https://gradle.org/docs/current/userguide/custom_plugins.html。

下一章,我们将会讨论持续集成(Continuous Integration:CI)的重要性。适时有个好的CI系统,我们可以使用一次点击构建、测试和配置app与library。总之持续集成因此是自动化构建的很重要的一部分。

你可能感兴趣的:(gradle)