В этой статье мы поговорим о том, как наша команда перешла на Kotlin DSL для описания Gradle файлов, и как в итоге мы получили типы сборок, описанные классами и имеющие общий интерфейс.

Меня зовут Давид, и я с 2018 года разрабатываю в Константе приложения под платформу Android, как большие и нацеленные на аудиторию в несколько сотен тысяч уникальных пользователей, так и рассчитанные на внутреннее использование. Вообще, в Константе много проектов, каждый из которых по-своему интересен и уникален. Например, есть приложения, ориентированные на несколько разных рынков, а есть мессенджер, заточенный на конфиденциальность передаваемых данных, который я сейчас делаю с нуля, и он пока используется внутри компании.

У нас большое сообщество Android разработчиков, и мы часто делимся опытом не только внутри команды, но и с коллегами в сети. В рамках одного из наших проектов мы решили попробовать кое-что новое, а именно - отказаться от Groovy в пользу Kotlin DSL для Gradle файлов. Об этом я вам и расскажу.

Решение использовать Kotlin DSL было принято по нескольким причинам:

  • удобство. Код на Kotlin DSL более читабелен, компактен и дает больше возможностей в рамках традиционного IDE;

  • любовь к Kotlin. Этот язык перспективен и прекрасен, и мы стараемся пользоваться его прелестями, где только можно;

  • Рекомендация от Google.

Итак, мы приступили к написанию скриптов. Для начала определили, какие типы сборок будут в проекте. В итоге остановились на таком варианте:

  • debug - тип по-умолчанию. Используется для отладки во время разработки;

  • qa - сборка, используемая QA инженерами внутри команды для тестирования функционала в рамках конкретной задачи;

  • preProd - сборка, используемая QA инженерами внутри команды для проведения регрессионного тестирования (a.k.a release-candidate);

  • release - сборка для конечных пользователей.

Этот список может отличаться от выбранного вами. Наш подход позволяет безболезненно добавлять / убирать типы сборок.

После этого мы добавили поддержку Kotlin DSL в проект. Вот пошаговая инструкция:

  • добавить папку с названием buildSrc на уровне проекта;

  • создать в ней файл с названием build.gradle.kts;

  • добавить туда скрипт:

import org.gradle.kotlin.dsl.`kotlin-dsl`
plugins {
	kotlin-dsl
}
repositories {
	jcenter()
}
  • переименовать все файлы формата .gradle в .gradle.kts, согласно инструкции.

Выполнив эти шаги, мы стали счастливыми обладателями хайпового современного проекта.

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

buildTypes{
	getByName("debug"){
		applicationIdSuffix = ".debug"
}
	create("qa"){
		applicationIdSuffix = ".qa"
		initWith(getByName("debug"))
		isMinifyEnabled = false
	}
	create("preProd"){
		applicationIdSuffix = ".preprod"
		isMinifyEnabled = true
		proguardFiles(
		getDefaultProguardFile("proguard-android-optimize.txt"),
		"proguard-rules.pro"
		)
	}
	create("release"){
		applicationIdSuffix = ".release"
		isMinifyEnabled = true
		proguardFiles(
		getDefaultProguardFile("proguard-android-optimize.txt"),
		"proguard-rules.pro"
		)
	}
}

Вроде бы не очень плохо, на первый взгляд. Только мы выбрали путь многомодульности, а копировать и вставлять один и тот же код в каждый модуль не очень хотелось. В связи с этим мы взялись за поиски более грамотного решения…. И нашли!

В первую очередь, мы создали интерфейс AppBuild в папке buildSrc, чтобы иметь к нему доступ из Gradle. Получилось так:

interface AppBuild {
	val name: String

	val suffix: String

	val isDebuggable: Boolean

	val signingParams: SigningParams?

}

Затем мы создали Build классы, реализующие этот интерфейс. Ниже показан класс для QA:

object QaBuild : AppBuild {
	override val name: String
    	get() = "qa"

	override val suffix: String
    	get() = ".$name"

	override val isDebuggable: Boolean
    	get() = true

	override val signingParams: SigningParams?
    	get() = null

}

Затем в AppBuild.companionObject добавили метод, возвращающий список всех Build классов:

interface AppBuild {

	companion object {
    	fun getAll(): List<AppBuild> =listOf(
        	QaBuild,
        	PreProdBuild,
        	ReleaseBuild
    	)
	}

	val name: String

	val suffix: String

	val isDebuggable: Boolean

	val signingParams: SigningParams?

}

После того, как мы получили объектно-ориентированные Build настройки, осталось добавить их в Gradle:

buildTypes{
	AppBuild
	        .getAll()
	        .forEach { build->
							create(build.name){
									applicationIdSuffix = build.suffix
	                isMinifyEnabled = build.isMinifyEnabled
	                if (isDebuggable) {
	                   initWith(getByName("debug"))
	                }
	                signingParams
	                   ?.let { params ->
	                      signingConfig = signingConfigs
	                                         .getByName(build.name)
	                      proguardFiles(
	                          getDefaultProguardFile(params.proguard),
	                          params.proguardRules
	                      )
	                   }
							}
	        }
}

И SigningConfigs:

signingConfigs{
			AppBuild
        .getAll()
        .forEach { build->
						create(build.name){
 									signingParams
                   ?.let { params ->
                       storeFile = file(params.keystoreLocation)
                       storePassword = params.keystorePassword
                       keyAlias = params.aliasName
                       keyPassword = params.aliasPassword                   }
						}
        }
}

Это все… Впоследствии мы получили BuildVariants с типами debugqapreProd и release. При этом мы не создавали отдельный тип debug, так как он уже был настроен изначально.

Мы выделили для себя такие плюсы этого подхода:

  • читабельность: все типы находятся в одном месте, а их настройки указаны в виде свойств;

  • расширяемость: в любой момент мы можем добавить новый build с его настройками, и все, что от нас потребуется — это Sync project;

  • гибкость: если нам понадобится сменить путь keystoresuffix или что-то еще, мы сделаем это всего один раз;

  • удобство при многомодульности: мы не пишем про все build типы во всех модулях отдельно, а всего лишь добавляем небольшой кусок кода;

Если говорить о минусах использования Kotlin DSL, обратили внимание на следующее:

  • мало документации в сети. Наша команда потратила немало времени на переход из Groovy на Kotlin DSL.

  • время сборки увеличивается. Об этом написано в официальной документации. Но стоит отметить, что при грамотном подходе к проекту (разбиение на модули и правильная настройка зависимостей) разницу не будете ощущать.

Вот так наша команда перешла на Kotlin DSL, надеемся, что наша статья будет полезна тем, кто только планирует этот переход.

Спасибо за внимание. До новых интересных задач!

Комментарии (3)


  1. nimishin
    07.06.2022 11:02

    Смелые ребята, мне тоже нравится Kotlin DSL, но maven - надежнее))


  1. DarthBeleg
    07.06.2022 11:03
    +2

    Я правильно понял, что вы копируете код, создающий варианты в каждый модуль вручную? Это все еще выглядит слишком трудоемко - что, например, если вы захотите конфигурировать еще какой-нибудь параметр варианта? Если вы хотите применять один и тот же набор вариантов для всех модулей, то у Gradle есть намного более простое и идиоматическое решение для этой задачи - precompiled script plugins, также известные как convention plugins. Можно написать один файл в buildSrc, например my-android-conventions.gradle.kts, в который и поместить всё конфигурирование вариантов, в виде DSL, как у вас это было изначально:

    plugins {
      id("com.android.application")  // без версии
    }
    
    android {
      buildTypes{
    	  getByName("debug"){
    		  applicationIdSuffix = ".debug"
        }
        // ...
      }
      // ...
    }

    Единственный нюанс - для того, чтобы иметь возможность применять Android-плагин из вашего плагина, нужно добавить его в implementation classpath buildSrc, написав в buildSrc/build.gradle.kts:

    repositories {
        google()
    }
    
    dependencies {
        implementation("com.android.tools.build:gradle:7.2.0")
    }

    После этого в модулях достаточно просто применить этот плагин:

    plugins {
      id("my-android-conventions")
    }


    1. Zakotlin Автор
      07.06.2022 11:17
      +1

      Добрый день. Спасибо за комментарий.

      Мы в модулях пишем код создания вариантов на абстрактном уровне, без деталей. При добавления нового варианта достаточно создать реализацию интерфейса и добавить в метод getAll.

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