Предисловие


Из далекого 2012, на просторах Хабра мне запомнился коммент:


… если нет корпуса с самого начала, то устройство так и останется
недоделанным, ляжет пылиться на полке...

Топик далеко не про хардварную составляющую. Разбирая свою проблему, я убедился в верности сия суждения и постарался навести порядок на своей пыльной полке.


Недавно ко мне обратился заказчик, который попросил добавить в его проект поддержку нескольких сервисов. Задача заключалась в том, что мне нужно было подключить сервис "А" и перед выкладкой приложения в продакшн, обкатать этот сервис на тестовом окружении. Я решил проанализировать свои предыдущие решения и… ужаснулся. 


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


Проблема


Google дает нам возможность пробрасывать кастомные значения для каждой
сборки.


android {
  //...
  buildTypes {
    release {
      buildConfigField("String", "HOST_URL", "\"prod.com\"")
    }
    debug {
      buildConfigField("String", "HOST_URL", "\"debug.com\"")
    }
  }
}

После анализа build.gradle скрипта, android tools заберет все значения buildConfigFileds из buildTypes и productFlavors и сгенерирует BuildConfig файлы для каждого типа сборок:


public final class BuildConfig {
  //...
  // Fields from build type: release
  public static final String HOST_URL = "prod.com";
}

Никакой проблемы, на первый взгляд. Особенно, когда в вашем приложении не столь много флейворов и кастомных значений. В моем проекте их было >20 и 3 окружения (internal/alpha/production). Очевидно, что проблема для меня была одна?-?избавиться от бойлерплейта.


Не менее важная проблема?-?значения перменных окружения не должны быть отражены в вашем проекте. Даже в конфигурационном файле. Вы обязаны затрекать через VCS ваш build.gradle конфиг. Но вы не должны прописывать ваши ключи напрямую, для этого, вам необходим сторонний механизм(например файл, сервисы вашего CI). В моей практике было несколько проектов, где для release production сборки у меня не было доступа к значениям некоторых библиотек. Это уже проблема бизнеса и в его интересах не делать лишних затрат. Вы не должны использовать ключи предназначенные для продакшена во время отладки или внутреннего тестирования.


Способ решения проблемы


В одном из старых проектов, для хранения значений переменных окружения, мы использовали простые .properties файлы, которые предоставляли доступ к полям через классический key:value map. Проблему биндинга данный подход не решает. Но он решает проблему поставки данных, который следует применить. Кроме того, мы можем взять за основу .properties файлы как определенного рода контракт предоставления данных. 


Если вернуться чуть назад, у нас есть промежуточный этап: из buildConfigField в поле класса BuildConfig. Но кто же это делает? Все довольно банально, за это отвечает gradle plugin который вы подключаете абсолютно во всех проектах Android.


apply plugin: "com.android.application"

Именно он отвечает за то, что после анализа вашего build.gradle файла будет сгенерирован класс BuildConfig для каждого флейвора со своим набором полей. Таким образом, я могу написать свое лекраство, которое расширит возможности com.android.application и избавит
меня от этой головной боли. 


Решение проблемы выглядит следующим образом: предоставить контракт,
в котором будут описаны все ключи и значения для всех сборок.
Разложить конфигурационные файлы на подтипы. Отдать все плагину.


drawing

Решение


Выше мы разобрались со структурой решения, осталось дело за малым — воплотить все это в жизнь. Казалось бы, тривиальное решение и проблему можно решить простым расширением билд файла. Изначально, я так и поступил.


Раскрыть решение
```groovy
class Constants {
    // Environments properties path pattern, store your config files in each folders of pattern
    static final CONFIG_PROPERTY_PATTERN = "config/%s/config.properties"
}

android.buildTypes.all { buildType ->
    buildConfigFields(buildType, buildType.name)
}

android.applicationVariants.all { appVariant ->
    buildConfigFields(appVariant, appVariant.flavorName)
}

private def buildConfigFields(Object variant, String variantName) {
    def properties = getProperties(variantName)
    properties.each { key, value ->
        variant.buildConfigField(
                parseValueType(value),
                toConfigKey(key),
                value
        )
    }
}

// Convert config property key to java constant style
private def toConfigKey(String key) {
    return key.replaceAll("(\\.)|(-)", "_")
            .toUpperCase()
}

// Parse configuration value type 
private def parseValueType(String value) {
    if (value == null) {
        throw new NullPointerException("Missing configuration value")
    }
    if (value =~ "[0-9]*L" ) {
        return "Long"
    }
    if (value.isInteger()) {
        return "Integer"
    }
    if (value.isFloat()) {
        return "Float"
    }
    if ("true" == value.toLowerCase() || "false" == value.toLowerCase()) {
        return "Boolean"
    }
    return "String"
}

private def getProperties(String variantName) {
    def propertiesPath = String.format(
            Constants.CONFIG_PROPERTY_PATTERN,
            variantName
    )
    def propertiesFile = rootProject.file(propertiesPath)
    def properties = new Properties()
    // Do nothing, when configuration file doesn't exists
    if (propertiesFile.exists()) {
        properties.load(new FileInputStream(propertiesFile))
    }
    return properties
}
```

А вот тут сразу же возникли те трудности, о которых я и не задумывался — пыльная полка.Я решил "продать" свое реше коллегам. Я подготовил доку, пнул дело на обсуждение и… Понял, что все мы люди, а программисты — это ленивые люди. Никому не хочется вставлять неизвестный ему участок кода в проект, его же по хорошему нужно изучить, прочитать? А вдруг он нерабочий? А вдруг он делает еще что-то не так? Это же груви, а я его не знаю и непонятно как с ним работать. А уже давно переехал на котлин скрипт, а портировать с груви я не умею и проч.


Самое интересное, что все эти суждения уже исходили от меня, т.к. я понял, что меня такая интеграция решения не устраивает. Плюс, я заметил несколько моментов, которые мне бы очень хотелось бы улучшить. Внедрив решение в проект А, мне бы хотелось бы его поддержать в проекте Б. Выход один — надо писать плагин.


А какие же проблемы решит плагин и его удаленная поставка пользователю?


  • проблему ленивого програмиста. Нам лень углубляться в корень проблемы и возможные способы его решения. Нам куда проще взять что-то, что былоуже сделано до тебя и использовать это.
  • поддержка. Она включает в себя поддержку кода, его развитие и расшириения возможностей. При решении своей проблемы, я решал только проброспеременных окружения только лишь в код, совсем позабыв о возможности проброса в ресурсы.
  • качество кода. Бытует такое мнение, что некоторые разработчики даже не смотрят на open source код, который не покрыт тестами. На дворе 2019 и мы с легкостью можем подключить себе сервисы для отслеживания качества кода https://sonarcloud.io либо https://codecov.io/
  • конфигурация. Расширение билд файла заставляет меня изучить этот код и внести изменения вручную. В моем случае — мне не всегда нужно использовать конфигурацию для buildTypes либо productFlavors, хочу что-то одно или все сразу.
  • уборка пыльной полки. Я наконец-то навел порядок на одной из них и смог это решение по-дальше своей комнатушки.

В подробности и проблемы при написании плагина я вдаваться не буду, тянет на новый топик. Взять решение с его интеграцией, предложить свою идею либо внести свой вклад можно
здесь.

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


  1. androidovshchik
    06.08.2019 17:34

    В моем проекте их было >20 и 3 окружения (internal/alpha/production)

    Как вариант думаю можно было просто подключить 3 дополнительных файла на groovy, где задать все проперти, ну и как бы без парсера обойтись и чтения громоздкого


    1. TranE91 Автор
      06.08.2019 23:57

      Такой подход не решает основной проблемы — избавление от бойлерплейта.
      За ней же следует пораждающая проблема поддержки.