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的結果如下:

這裏寫圖片描述

正如在過程截圖看到的,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。總之持續集成因此是自動化構建的很重要的一部分。

發佈了39 篇原創文章 · 獲贊 77 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章