Android Environments


From distant 2012, in the open spaces of Habr, I remember the comment:

... if there is no case from the very beginning, then the device will remain
unfinished, will fall dust on a shelf ...

The topic is far from a hardware component. Analyzing my problem, I became convinced of the correctness of this judgment and tried to put things in order on my dusty shelf.

Recently I was approached by a customer who asked me to add support for several services to his project. The task was that I needed to connect the “A” service and, before putting the application into production, run this service on a test environment. I decided to analyze my previous decisions and ... was horrified.

For various types of assemblies, I used various configuration files with the description of environment variables. But the problem was that just to forward the value into the real code, it was necessary to write the same code for each type.


Google gives us the ability to forward custom values ​​for each
assembly .

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

After analyzing the build.gradle script, android tools will take all the values ​​of buildConfigFileds from buildTypes and productFlavors and generate BuildConfig files for each type of assembly:

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

No problem at first glance. Especially when there are not so many flavors and custom values ​​in your application. In my project there were> 20 and 3 environments (internal / alpha / production). Obviously, there was only one problem for me - get rid of the boilerplate.

An equally important problem is that the values ​​of environment variables should not be reflected in your project. Even in the configuration file. You must check your build.gradle config via VCS. But you should not register your keys directly, for this, you need a third-party mechanism (for example, a file, the services of your CI). In my practice, there were several projects where for release production assembly I did not have access to the values ​​of some libraries. This is already a business problem and in its interests not to make unnecessary costs. You should not use keys intended for production during debugging or internal testing.

The way to solve the problem

In one of the old projects, to store the values ​​of environment variables, we used simple .properties files that provided access to the fields through the classic key: value map. This approach does not solve the binding problem. But it solves the problem of data delivery, which should be applied. In addition, we can take as a basis .properties files as a certain kind of contract for the provision of data.

If we go back a little, we have an intermediate step: from buildConfigField to the field of the BuildConfig class. But who does this? Everything is pretty corny, the gradle plugin that you connect absolutely in all Android projects is responsible for this.

 apply plugin: "" 

It is he who is responsible for the fact that after analyzing your build.gradle file, the BuildConfig class will be generated for each flavor with its own set of fields. That way, I can write my own medicine that will expand the capabilities of and save
me from this headache.

The solution to the problem is as follows: provide a contract,
which will describe all the keys and values ​​for all assemblies.
Expand configuration files into subtypes. Give everything to the plugin.



Above we figured out the structure of the solution, the only thing left to do is to bring it all to life. It would seem that a trivial solution and a problem can be solved by a simple build file extension. Initially, I did so.

Reveal solution
 ```groovy class Constants { // Environments properties path pattern, store your config files in each folders of pattern static final CONFIG_PROPERTY_PATTERN = "config/%s/" } android.buildTypes.all { buildType -> buildConfigFields(buildType, } 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 } ``` 

And here at once those difficulties arose that I hadn’t thought of - a dusty regiment. I decided to “sell” my decision to my colleagues. I prepared a dock, kicked the matter up for discussion, and ... I realized that we are all people, and programmers are lazy people. Nobody wants to embed a piece of code unknown to him in the project, does it need to be studied, read for good? What if he’s not working? What if he is doing something else wrong? It's groovy, but I don’t know him and it is not clear how to work with him. And a script has already moved to Kotlin for a long time, and I don’t know how to port from grooves and so on.

The most interesting thing is that all these judgments have already come from me, because I realized that such integration of the solution does not suit me. Plus, I noticed a few points that I would really like to improve. Having implemented the solution in project A, I would like to support it in project B. There is only one way out - you need to write a plug-in.

And what problems will the plug-in and its remote delivery to the user solve?

I won’t go into details and problems when writing the plugin, it is pulling on a new topic. You can make a decision with its integration, propose your idea or make your contribution
here .


All Articles