Большинство программистов, разрабатывающих для Android, хотя бы слышали о системе автоматической сборки Gradle. При этом, по моим наблюдениям, лишь немногие из использующих эту систему кодеров уделяют достаточно времени, чтобы как следует изучить ее возможности :). Самая частая причина звучит так: «Да ладно, это ж просто скрипт сборки, у меня есть задачи поважнее».
А ведь на самом деле Gradle может быть очень полезен как для простой настройки сборки, так и для решения весьма нестандартных задач! Об этом и пойдет речь сегодня.

 

Android Gradle plugin

Gradle сам по себе ничего не знает о том, как билдить Android-проект, в этом ему помогает плагин, который разрабатывается вместе с Android SDK. Если ты только недавно начал осваивать программирование под Android, то мог и не заметить, что в главном сборочном скрипте build.gradle студия самостоятельно добавляет зависимость от этого плагина.

buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        // Android Gradle plugin добавляется здесь
        classpath 'com.android.tools.build:gradle:2.1.3'
    }
}

А в скрипте твоего основного модуля этот плагин автоматически подключается строчкой apply plugin: 'com.android.application'. Именно поэтому у тебя в скрипте есть секция android { ... }, в которой ты указываешь версию Build Tools, версии SDK для сборки и прочее.

Перед тем как мы попытаемся глубже разобраться в работе самого Gradle, я покажу тебе несколько полезных вещей, которые умеет делать этот плагин и о которых ты мог не знать.

 

Добавляем свои поля в BuildConfig

BuildConfig — это автоматически генерируемый при сборке класс, который содержит только константы. Этот класс генерируется отдельно для каждого модуля в твоем проекте и по умолчанию включает в себя информацию об ID приложения, версии, типе сборки.

// Типичный BuildConfig
public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "ru.ingos.ingosview";
  public static final int VERSION_CODE = 37;
  public static final String VERSION_NAME = "2.9-offline";
}

Редактирование вручную этого файла бесполезно, так как он все равно перезатрется новыми данными при сборке. Зато Android-плагин может добавлять в него те поля, которые ты скажешь.

android {

    defaultConfig {
        applicationId "example.myawesomeapp"
        minSdkVersion 16
        targetSdkVersion 24
        versionCode 1
        versionName "MyApp-v1.0"
        buildConfigField "String", "SERVER", '"https://my-server.example"'
        buildConfigField "long", "TIMEOUT ", "${1000 * 60 * 5}" // 5 минут
    }
    // Прочее
}

Первый параметр — тип константы, второй — имя, третий — значение, все просто. Заметь, что значение поля TIMEOUT вычисляется на этапе сборки и в BuildConfig попадет уже как число 300 000. Теперь ты можешь конфигурировать свой любимый HTTP-клиент, просто ссылаясь на константы в BuildConfig.

// Пример использования BuildConfig
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setConnectTimeout(BuildConfig.TIMEOUT, TimeUnit.MILLISECONDS);
okHttpClient.newCall(new Request.Builder().url(BuildConfig.SERVER).build());
 

Добавляем свои данные в ресурсы

Принцип точно такой же, что и с BuildConfig, но позволяет добавлять значения в файл ресурсов. Но зачем добавлять ресурс из конфига, если проще это сделать, как обычно, в XML-файле? Просто потому, что в скрипте, так же как и в случае с BuildConfig.TIMEOUT, значение ресурса можно вычислить. Например, сохранить дату сборки:

resValue "string", "BUILD_TIME", "${System.currentTimeSeconds()}"

Gradle создаст специальный файл generated.xml примерно такого содержания (только, разумеется, с правильными угловыми скобочками):

[?xml version="1.0" encoding="utf-8"?]
[resources]
    [!-- Automatically generated file. DO NOT MODIFY --]
    [!-- Values from default config. --]
    [string name="BUILD_TIME" translatable="false"]1471574224[/string]
[/resources]

И пусть тебя не смущает, что мы храним время в формате String. К сожалению, Android SDK не умеет хранить в ресурсах long, а в 32-битный integer время не влезет.

 

Создаем разные варианты сборки

Пожалуй, уже все Android-программисты знают о существовании встроенных типов сборок debug и release. Чуть меньше — о том, что можно создавать свои типы сборок. Еще меньше тех, кто дополнительно применяет productFlavors. Но давай по порядку.


...Все Android-программисты знают о существовании встроенных типов сборок debug и release. Чуть меньше — о том, что можно создавать свои типы сборок. Еще меньше тех, кто дополнительно применяет productFlavors

Мы используем build types, чтобы иметь возможность собирать приложение с существенными отличиями. Эти отличия обычно связаны с тем, как мы собираем приложение: для отладки или для релиза, с обфускацией кода или без, каким сертификатом оно будет подписано.

buildTypes {

    release {
        minifyEnabled true // Включаем обфускацию
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-all.txt'
        signingConfig signingConfigs.release // Указываем релизный конфиг для подписывания
    }

    debug {
        minifyEnabled false // Отключаем обфускацию
        signingConfig signingConfigs.debug // Указываем отладочный конфиг для подписывания
    }

    qa {
        minifyEnabled false // Отключаем обфускацию
        signingConfig signingConfigs.debug // Указываем отладочный конфиг для подписывания
        testCoverageEnabled true // Включаем анализ покрытия тестами
    }

}

Чтобы собрать нужный тип, выполняем команду gradle assemble<ИмяТипаСборки>, например gradle assembleDebug или gradle assembleQa.

INFO


Есть два пути настройки Gradle. Ты можешь установить его на машину самостоятельно или использовать Gradle Wrapper внутри проекта. В первом случае Gradle будет доступен тебе глобально через команду gradle из консоли. Во втором случае сборку можно запускать через специальную программу-обертку — gradlew. Второй способ предпочтительнее, так как может работать с любой версией Gradle без переустановки. Тем более что при создании проекта в Android Studio этот способ работает по умолчанию. Подробнее о Gradle Wrapper ты можешь почитать по ссылке.

Product flavors дополняют build types и вносят еще один уровень гибкости в настройку сборки. Используй их, когда нужно, скажем так, не глобально изменить приложение, — это могут быть брендинг (иконки, цвета, тексты), окружение (адрес сервера, платформа, trial- или pro-версии).

productFlavors {

    trial {
        versionName "MyAwesomeApp-trial"
        buildConfigField "String", "SERVER", '"https://trial.my-server.example"'
    }

    pro {
        versionName "MyAwesomeApp-pro"
        buildConfigField "String", "SERVER", '"https://pro.my-server.example"'
    }
}

Build type и product flavor в сумме дают так называемый итоговый Build Variant, собрать который можно по схеме gradle assemble<ИмяПродукта><ИмяТипаСборки>. Если ты хочешь запустить эти сборки не из консоли, а из студии, открой вкладку Build Variants и выбери то, что тебе нужно, из списка, как на рис. 1.

Рис. 1. Выбор Build Variant в Android Studio
Рис. 1. Выбор Build Variant в Android Studio

gradlew assembleTrialRelease
gradlew assembleProDebug
gradlew assembleProQa

Каждая из секций buildTypes и productFlavors {...} может иметь свои buildConfigField {...}, resValue, versionName и другие параметры, которые будут приоритетнее, чем те, что объявлены в defaultConfig {...}.

 

Настраиваем информацию о приложении

Имея несколько вариантов сборок, ты точно захочешь их идентифицировать или различать после установки. Как раз для этого у Android-плагина есть парочка параметров — applicationIdSuffix и versionNameSuffix, которые добавляют к существующему ID приложения и к существующей версии то, что ты пожелаешь.

android {
    defaultConfig {
        versionName "MyAwesomeApp"
        applicationId "example.myawesomeapp"
    }

    buildTypes {
        release {
            applicationIdSuffix ".release"
        }
        debug {
            applicationIdSuffix ".debug"
        }
    }

    productFlavors {

        trial {
            versionNameSuffix "-trial"
        }

        pro {
            versionNameSuffix "-pro"
        }
    }
}

С таким конфигом команда gradle assembleTrialRelease соберет тебе приложение с applicationId="example.myawesomeapp.release" и названием версии MyAwesomeApp-trial.

Заканчивая тему с Android-плагином для Gradle, нужно сказать, что это только часть его возможностей. Плагин постоянно развивается и приобретает новые фичи. На сайте tools.android.com есть подробный гайд по его использованию.

 

Gradle DSL

А теперь давай попробуем разобраться, почему конфигурация сборки в Gradle называется скриптом, из чего состоит этот скрипт и почему он выглядит так, как выглядит. Gradle часто называют объединением систем сборки Ant и Maven.

С одной стороны, Gradle, как и Maven, обеспечивает декларативный подход к сборке, когда программист лишь объявляет нужные значения и параметры, а система сама знает, как сделать всю остальную работу самостоятельно. Именно этим мы занимались в предыдущей части.

С другой стороны, Gradle, как и Ant, умеет выполнять команды, но пишутся они не в XML-файле, а уже с помощью Gradle DSL (domain-specific programming language), написанном на Groovy. В мире Gradle эти команды называются Tasks (задачи). Задачи можно делать зависимыми от других задач и таким образом строить граф их выполнения. По сути, цепочка задач и установленные параметры и есть скрипт сборки приложения.

В прошлой части статьи, когда мы выполняли команды вроде gradle assembleRelease, на самом деле мы запускали уже готовую одноименную задачу. Она не взялась из ниоткуда, ее нам подготовил Android-плагин. Ты всегда можешь посмотреть список доступных команд, выполнив gradle tasks. Попробуй, и ты увидишь, как много задач тебе уже предоставлено.

Рис. 2. Типичный набор задач в Android-проекте
Рис. 2. Типичный набор задач в Android-проекте

Стандартные команды ты можешь изучить, запуская их с помощью gradle help или gradle install. А как насчет собственных задач? Легко — давай же скорее напишем Hello Gradle!

task hello {
    doLast {
        println 'Hello world!'
    }
}

Добавь эту задачу в свой build-скрипт, и ты сможешь запустить ее gradle hello. Она появится также в списке задач (gradle tasks) в разделе Other tasks. Если ты знаком с Groovy, ты сразу заметишь, что тело задачи — это просто замыкание (closure) с кодом, печатающим слова. Вся мощь Gradle и заключается в том, что в теле задачи можно писать Groovy-код, а значит, можно создавать задачи, делающие что угодно, если это можно уложить в программный код.

Прежде чем мы напишем что-то действительно полезное, давай я тебе покажу еще пару примеров манипулирования задачами.

 

Пример 1: добавляем зависимости к задаче

task hello << {
    println 'Hello '
}

task world << {
    println 'world'
}

task greetings (dependsOn: [hello, world])

Мы написали две задачи, печатающие отдельно слова Hello и world. Операция << эквивалентна вызову doLast{...} и используется для краткости записи. Последняя задача greetings принимает в качестве зависимости массив других задач. Если запустить ее, то она самостоятельно запустит все задачи, от которых зависит.

gradle greetings

:app:hello
Hello
:app:world
world
:app:greetings

Есть еще один вариант установки зависимостей:

greetings.dependsOn(hello)
greetings.dependsOn(world)

Этот способ работает, потому что задачи в Gradle — это объекты, у них есть методы, их можно передавать в качестве параметра в функции.

 

Пример 2: динамическое создание задач

Подобно тому, как Android-плагин автоматически генерирует задачи под твои build types и product flavors, ты сам можешь генерировать свои задачи.

5.times { counter ->
    task "task$counter" << {
        println "I'm task number $counter"
    }
}

Такой скрипт создаст тебе пять задач с именами task0, tasl1 и так далее.

 

Практика

ОK, ближе к делу, давай напишем что-нибудь полезное. Многие проекты состоят не только из одного основного модуля app, но и из нескольких вспомогательных, каждый из которых имеет свой скрипт build.gradle со своими настройками. При обновлении Android SDK становится утомительно обновлять каждый из скриптов отдельно и редактировать в них compileSdkVersion, buildToolsVersion, targetSdkVersion... Зато можно написать задачу, которая сделает это самостоятельно. Открой скрипт build.gradle в корне своего проекта, найди в нем секцию allprojects {...} и добавь такой код:

allprojects {
    subprojects { subproject ->
        afterEvaluate {
            if ((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
                android {
                    compileSdkVersion 24
                    buildToolsVersion '23.0.3'

                    defaultConfig {
                        targetSdkVersion 24
                    }
                }
            }
        }
    }

    // Прочее
}

У Gradle API есть метод subprojects, который принимает на вход замыкание и вызывает его для каждого подмодуля в проекте. В теле функции-замыкания мы проверяем, относится ли модуль к Android, и, если да, заменяем все, что относится к версии Build Tools и версии SDK.

Следующая задача посложнее: автоматизировать подстановку версии приложения (versionCode и versionName). Давай представим, что в проекте используется Git, каждый релиз помечается соответствующим тегом в формате release2.3.4. Тогда в качестве versionName можно будет брать имя самого свежего тега, а versionCode будет равняться количеству этих тегов. В качестве бонуса сгенерируем файл с историей релизов.

Для начала нужно написать функцию, вытаскивающую с Git всю нужную информацию.

def getGitTags(){
    ByteArrayOutputStream stdOut = new ByteArrayOutputStream()
    exec {
        commandLine 'git', 'for-each-ref', '--sort=authordate', '--format', '%(refname:short)-%(contents:subject)', 'refs/tags/release*'
        standardOutput = stdOut
    }

    return stdOut.toString().trim().split("\n")
}

Суть функции в том, что она выполняет консольную команду git for-each-ref, доставая все теги, начинающиеся с release, в формате ИмяТега-СообщениеТега и возвращает их списком строк. Получается что-то вроде:

release2.1.2-Improvements
release2.2.45-New features
release2.3.4-Hot fix

Реальное значение зависит, конечно, от того, что на самом деле лежит в Git проекта. Эту функцию мы можем использовать в секции android, чтобы заполнить значения versionCode и versionName:

android {
    def gitTags = getGitTags()

    defaultConfig {
        versionCode gitTags.size() // Номер версии = количество релизов
        versionName gitTags.last().split('-')[0] // Название версии — первая часть до дефиса в названии тега
    }
}

Автоподстановку версии мы настроили. Осталось записать список релизов в файл. Сделаем для этого новую задачу:

task printVersions << {
    def list = getGitTags().join("\n")
    new File("history.txt").withWriter{ it << list}
}

Так как Groovy — это дополнение к Java, у тебя в распоряжении весь стандартный Java API. Здесь, например, нам пригодился стандартный Java-класс File. Чтобы генерировать этот файл не вручную, а вместе с билдом, подцепим нашу задачу к какой-нибудь из уже имеющихся, например к preBuild:

preBuild.dependsOn('printVersions')
 

Итого

Мы посмотрели на штатные возможности Android-плагина для Gradle, немного поковыряли Gradle API, поучились писать свои задачи. Разумеется, все это только верхушка айсберга. Вокруг Gradle уже сформировалось большое комьюнити, и оно развивает и создает свои плагины: для деплоя, для тестирования, для статистики и кучу других, которые могут сделать твою жизнь лучше. А если ты не найдешь то, что тебе нужно, то ты сможешь написать свой плагин или задачу. Успехов!

WWW


Несколько ресурсов с подборкой полезных Gradle-плагинов:
Android Arsenal
Best gradle plugins for Android dev
Essential Gradle Plugins for Android Development

Видео доклада о внутреннем устройстве Gradle (на английском):
Gradle under the hood (Dawid Kublik)

1 комментарий

  1. Аватар

    clicker314

    04.10.2016 в 14:07

    Уважаемая редакция, верните голосование за посты. Я поставлю этому посту +

Оставить мнение