В
першій частині ми говорили про таски та їх стадії життєвого циклу. Але вже після публікації я зрозумів що перед ти як ми поринемо в особливості Gradle важливо зрозуміти з чим саме ми маємо справу - зрозуміти його синтаксис та перестати боятися вигляду файлу
build.gradle. І за допомогою цієї статті я спробую заповнити дану прогалину.
Синтаксис
Скрипти Gradle написані мовою
Groovy, тож до того як ми почнемо їх аналізувати, я хотів би зачепити (коротко) деякі ключові концепти
Groovy. Синтаксис Groovy деяким чином схожий на Java, тож сподіваюсь у вас не буде великих проблем з його розумінням.
Якщо з Groovy почуваєтесь на ти - не бачу проблем щоб не пропустити декілька абзаців.
Є один дужу важливий нюанс в Groovy, який вам необхідно зрозуміти для повного розуміння скриптів Gradle -
Closure.
Closures
Closure - це ключовий компонент який нам необхідно зрозуміти перед освоєнням Gradle. Closure це незалежний блок коду, який може приймати аргументи, повертати значення та бути присвоєним змінній. Це такий собі мікс між інтерфейсами
Callable,
Future, та покажчиком на функцію.
По суті це блок коду, який виконуються при виклику, а не при стоворенні. Подивимось на простй приклад Closure:
def myClosure = { println 'Hello world!' }
//виелик closure
myClosure()
#output: Hello world!
Або тут closure який приймає параметр:
def myClosure = {String str -> println str }
//виклик closure
myClosure('Hello world!')
#output: Hello world!
Або якщо closure приймає тільки один параметр, він може виглядати як
it:
def myClosure = {println it }
//виклик closure
myClosure('Hello world!')
#output: Hello world!
Чи коли closure приймає декілька параметрів:
def myClosure = {String str, int num -> println "$str : $num" }
//виклик closure
myClosure('my string', 21)
#output: my string : 21
Крім того, тип аргументу опціональний, далі бачимо спрощений приклад без типів:
def myClosure = {str, num -> println "$str : $num" }
//виклик closure
myClosure('my string', 21)
#output: my string : 21
Ще одна гарна можливість closure
може посилатися на змінні з поточного контексту(читай класу). За замовчуванням, поточний контекст - це клас створений в даному closure:
def myVar = 'Hello World!'
def myClosure = {println myVar}
myClosure()
#output: Hello world!
Ще одна можливість в тому що поточний контекст для closure може бути змінений викликом
Closure#setDelegate(). Ця можливість буде дуже важливою трохи пізніше:
def myClosure = {println myVar} //Зсилаємося на myVar з класу MyClass
MyClass m = new MyClass()
myClosure.setDelegate(m)
myClosure()
class MyClass {
def myVar = 'Hello from MyClass!'
}
#output: Hello from MyClass!
Як ви могли помітити, в момент коли ми створюємо closure, змінна
myVar ще не існує. І тут це повністю нормально - вона має бути присутньою в контексті closure в момент виконання.
В цьому випадку я модифікував теперішній стан контексту для closure прямо перед тим як виконати його, тож
myVar доступна.
Передача closure як аргумента
Користь від closures - в можливості передавати closure іншим методам, що допомагає нам відокремити логіку від вииконання.
Раніше ми також викоритсовували closure для передачі екземпляру іншого класу. А тепер переглянемо інші методи closure:
- Методи приймає один парамет - closure
myMethod(myClosure)
- Якщо метод приймає один парамет - дужки можна не використовувати
myMethod myClosure
- Внутрішній closure
myMethod {println 'Hello World'}
- Метод приймає два параметри
myMethod(arg1, myClosure)
- Те ж що й 4
myMethod(arg1, { println 'Hello World' })
- Якщо останній параметр closure - то його можна винести
myMethod(arg1) { println 'Hello World' }
Зверніть увагу на пункти 3 та 4. Нічого не нагадує? Це ж і є Gradle скрипти.
Gradle
З синтаксисом трохи ознайомилися, тепер розберемося як же, все-таки, це відноситься до скриптів Gradle? Подивимось на простий приклад скрипту Gradle і постараємось його зрозуміти:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
allprojects {
repositories {
jcenter()
}
}
Ви тільки подивіться! Знаючи синтаксис Groovy ми розуміємо що саме коїться в даному прикладі!
- Метод
buildscript що приймає значення closure:
def buildscript(Closure closure)
- Метод
allprojects приймає closure:
def allprojects(Closure closure)
...і так далі.
Прекрасно, але ця інформація сама по собі насправді не допомагає... Тепер треба визначити де саме це все виконується.
А відповіддю буде -
Project
Project
Це
ключ для розуміння скриптів Gradle:
Всі вирази на вищому рівні в скрипті build заделеговані в екземплярі Project
Це означає, що
Project - є початковою точкою.
Спробуємо знайти метод
buildscript.
Якщо будемо шукати
buildscript - ми знайдемо блок скрипту
buildscript {}. Секундочку.. А що ж це таке, чорт забирай??? Згідно з
документацією:
Блок скриптів це виклик методу який бере closure як параметр
Добре! Знайшли! Ось що насправді трапляється, коли ми викликаємо
buildscript { ... } - ми запускаємо метод
buildscript який приймає Closure.
Якщо продовжимо читати документацію по
buildscript то дізнаємось:
Delegates to:
ScriptHandler from buildscript. Це означає, що обсяг виконня для closure, який ми передаємо як вхідний параметр, буде змінений на
ScriptHandler. В нашому випадку ми передаємо closure який запускає метод
repositories(Closure) і метод
dependencies(Closure). З того часу як closure заделегований в
ScriptHandler, спробуємо знайти метод
dependencies в класі
ScriptHandler.
І ми його знаходимо -
void dependencies(Closure configureClosure), щ згідно з документацією, конфігурує залежність для скрипту. Тут ми натикаємось на ще один термін:
Executes the given closure against the DependencyHandler. Що означає
те ж саме що й "
lделегується до [чогось]" - цей closure буде виконано в рамках іншого класу (в нашому випадку -
DependencyHandler)
"delegates to [something]" та "configures [something]" - 2 вирази, що мають одне значення - closure буде виконано в зазначеному класі.
Gradle постійно використовує стратегію делегатів, отже важливо тут розуміти термінологію.
Для повноти, давайте подивимось, що трапиться коли ми виконаємо closure
{classpath 'com.android.tools.build:gradle:1.2.3'} в контексті
DependencyHandler. Згідно з документацією цей клас конфігурує залежності і має виглядати як:
<configurationName> <dependencyNotation1>
Отже в нашому closure ми зконфігуруємо
classpath для використання
com.android.tools.build:gradle:1.2.3 як залежності.
Блоки скриптів
За замовчуванням, існують попередньо встановлені блоки в
Project, але плагіни Gradle дозволяють додавати нові блоки самостійно!
Це означає, що якщо ви бачите щось на кшталт
something { ... } на верхньому рівні вашого білд скрипту і ви не можете знайти ні блок скриптів, ні метод, який приймає closure в документації - схоже на те, що один з плагінів, які ви використовувал, додав цей блок скриптів.
android Script block
Подивимось на скрипт білду Android
app/build.gradle за замовчуванням:
apply plugin: 'com.android.application'
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "com.trickyandroid.testapp"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
Як ми можемо бачити, здається, що тут має бути методи
android який приймає Closure як параметр. Але якщо ми спробуємо знайти такий метод в документації
Project то не знайдемо нічого подібного. Причина проста - такого методу просто не має :)
Якщо придивитися до скрипту build - ви побачите що перед запуском методу
android - ми застосовуємо плагін
com.android.application! Це і є відповідь на наше запитання! Android наслідує
Project з блоком скриптів
android (що є просто методом, який приймає Closure та делегує його до lкласу
1 AppExtension ).
Але де нам знайти документацію плагінів Android? ЇЇ можна завантажити з офіційного веб сайту
Android Tools.
Якщо ми відкриємо документацію AppExtension то знайдемо всі методи та атрибути, які необхідні.
Вправа
Спробуємо самотушки щось переналаштувати, бо ми тепер знаємо як це робиться :)
В AppExtension є блок скриптів
testOptions який делегує Closure в клас
TestOptions. Перейдемо до класу
TestOptions ми бачимо дві властивості:
reportDir та
resultsDir. Згідно з документацією,
reportDir несе відповідальність за місце розташування протоколу тестування. Змінимо його.
android {
......
testOptions {
reportDir "$rootDir/test_reports"
}
}
Ми використали властивість
rootDir з класу
Project що вказує на корінь папки проекту.
Тож якщо зараз ми запустимо
./gradlew connectedCheck, звіт про тестування відправиться в папку
[rootProject]/test_reports .
На практиці цього краще не робити.
Happy gradling!
Source.