среда, 24 июня 2015 г.

Про Gradle: #2 Синтаксис

В першій частині ми говорили про таски та їх стадії життєвого циклу. Але вже після публікації я зрозумів що перед ти як ми поринемо в особливості 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:
  1. Методи приймає один парамет - closure
    myMethod(myClosure)
  2. Якщо метод приймає один парамет - дужки можна не використовувати
    myMethod myClosure
  3. Внутрішній closure
    myMethod {println 'Hello World'}
  4. Метод приймає два параметри
    myMethod(arg1, myClosure)
  5. Те ж що й 4 
    myMethod(arg1, { println 'Hello World' })
  6. Якщо останній параметр 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.

Комментариев нет:

Отправить комментарий