/configuration/environment variables/flavors/flutter/Mobile development
Configure your Flutter Environment Properly
Unlike 99% of all articles, when the copy/paste approach is suggested to build an application for multiple environments, in this one I will focus on cases when you have exactly one entry point with an environment specific configuration being generated for your app with one small CLI command.
So it will be useful for Flutter developers that don’t have a lot of experience with CI/CD or projects that have multiple environments. For any other Flutter developers, this article will show how the build process can be simplified by the advertising package flutter_config to configure the native layer and environment_config package for the Dart layer, and how flutter_config can be used with environment_config to make things even simpler.
When you start developing a new mobile application, you usually don’t have problems with multiple environments and their configuration. But after some time, when it’s time to publish your work to production (especially if it’s a public project), you will face the issue that you need to configure both the production and development environments. Later, when the project starts to grow, quite likely you will need to set up more than that. For instance, in our project, we somehow need to deal with 5 different environments. Usually, frameworks provide different tools and techniques out of the box to answer this question, but it’s not obligatory. So let’s try to figure out what options Flutter has.
Flavors, what are they?
If you ever were curious about the different techniques of environment configuration in Flutter, you have definitely heard about Flavors.
Flavors in Flutter were basically inherited from the Android environment configuration approach. So setting them up is quite straightforward.
Specified variables can be used in XML files and in native code. You can easily find the extended configuration in various articles or in Android docs, so I won’t dive deep into that subject.
iOS Flavors are not quite Flavors
While dealing with Flavors in Android is quite simple, configuring them in iOS is a little bit harder, since iOS doesn’t have such a thing right out of the box. In order to make it work, the Flutter team decided to use Schemes and Configurations. The main pitfall of this approach is that you need to have a number of configurations, which equal the number of environments multiplied by 3 (in the worst case scenario). For instance, if you have test, stage and prod environments, since each env can be built in debug, profile and release modes, you will need to have configurations like: Debug, Release, Profile, Debug-test, Release-test, Profile-test, Debug-stage, Release-state, Profile-test and etc. And then variables from various configurations can be used in plists and native code.
And here we get to the final part of every Flutter application - Dart code.
So let’s tie Flavors and their variables to Dart code. And… wait, what? You can’t do that?
Yep, unfortunately, there is no way to directly get access to Flavor variables inside Dart code. Instead, almost every article suggests that you use the next approach:
But isn’t it a good approach? Let’s be honest - not quite.
We can start with the fact that storing your credentials from all your environments on GitHub or any other VCS is not the best idea. Another pain point of this approach is that you will need to have multiple config files, and changing or adding config variables could become quite problematic, especially if your configuration will start growing.
You could ask: so why is this approach for Dart code so bad, and basically the same approach for native code (Flavors) so good? Well, in fact, I’m not particularly excited about the Flavors approach either. But... it’s native code side, so potentially you may need to have different values per platform. We will try to solve this issue too in a few minutes. But before that, let’s focus on Dart code first.
So is there anything we can do about this? For sure, yes. We can create a CLI command that will generate a config file and you will have 1 entry point. There could be multiple options for how this can be done, so let’s take a look at other languages first.
What other languages suggest
Let’s take a look at the most common way how JS and PHP solve problems with different environment configurations.
Web is quite close to mobile apps, simply because it also runs in a client environment, so we will start from JS. Webpack (JS build tool) has a plugin called “DefinePlugin” that allows you to use env variables within your code during compile time. The idea behind this is quite simple. You are using global env variables in your code. During the build, these variables are replaced with actual values. This allows you to specify a different set of values for a particular environment build.
In PHP, on the other hand, we have a package called “composer-parameter-handler”. You can find information about it in the composer - PHP dependency manager. What it does is generate a YAML file based on the provided values from global environment variables, dist file or terminal arguments.
So what approach to choose? Well, JS’s approach is quite interesting, but PHP’s is much simpler and solves the exact same issue. So let’s use it and create a code generation script for this.
Let’s build your builder for the build 🙂
We will start with something simple - collecting data from command arguments.
First, create a Dart file with a ”main” function in it. It will be executed by default when we run this file from the terminal. Then, get data from arguments.
To do so, we will need a Dart package called args. This package allows you to retrieve entered arguments as HashMap.
You can define as many options as you want. Then, simply create your config file with the provided key/values. But what config type is better to choose?
Choose a config format
First, you can think of something like JSON, YAML or .env formats. All of them are quite simple, but the common issue for them is that all values will be Strings, and also you will need to import this file. Since it’s an Async operation, it means that you won’t be able to start your Flutter app until the config is loaded. You may think, “well, it’s not a big problem overall, I can live with it.” Sure, but why do you need to leave with it if you can easily fix it?
To solve all of this, we can simply use a Dart file with a config class in it. And it is actually easy to generate. To do so, we just need a package called code_builder. This package allows you to define the file structure, generate source code and even format it.
Just modify the set of fields based on the set of arguments that you are going to receive from command. And DartFormatter will do the rest. It will return the formatted string which can be written in a file like this.
And that’s all. You just generated your first config file. Now you can import it anywhere in a project and use data from it. Obviously, you will want to add the generated file to gitignore, which means that you will need to generate it during the build.
Integration with a build tool
So the next step, surprise, surprise - is to integrate it into your build tool 🙂 We will use CodeMagic as an example. This tool allows you to specify different variables per workflow:
And then you can add your command to be executed in the following way:
That’s pretty much all from the Dart side.
Now let’s solve the native question
As I promised earlier, now we will try to figure out something for the native portion of your app to simplify Flavors setup. To fix this, you can simply use a package called flutter_config
This package allows you to use values from the “.env” file inside your native code, including gradle config for Android and plist file for iOS. You can find info about setting this thing up in native for Android and iOS, so I won’t spend too much time explaining how to use this package, since the docs are quite descriptive and straightforward.
And at this point, you may ask: so what if I need to have the ability to configure native and Dart code? Should I extend the config generator to create a .env file too? Or maybe it’s better to simply use a “.env” file instead of a Dart config class?
Well, it’s your choice for sure, both options are fine. And I would say that probably the first variant will be better, simply because you will get benefits from Dart’s typing system, despite it being a bit more complicated. But what if I told you that you can use the first approach in a far more simple way?
Combine the best from both worlds
This package allows you to generate a Dart class and .env file from the same config. It can also generate fields with types other than String, or generate just a Dart class or just a .env file. It also has the ability to define custom Dart code for field values if needed (like a method call to get value and etc). There are a lot of different options, and you can find more info with some examples here.
But let’s return to our topic. To use this package, you need to define a config in pubspec.yaml or environment_config.yaml (also you can provide a path to any other YAML file during command run if needed) with any amount of fields.
Let’s define what we need to do next:
have a ENV key to be defined in Dart and .env
SOME_API_KEY defined just in a .env file for native layer
Last but not least, API_URL just in Dart class for Flutter code
In addition to that, let’s define that, during config generation, each of these fields should try to get value from the global env variables like:
APP_ENV for ENV key
SOME_API_KEY for SOME_API_KEY
API_URL for API_URL
Also, we will define some default values per each field.
A quick explanation about API_URL default value: as you may know, if you need to make API calls to your local environment, Android and iOS require different URLs. And to use Platform object, you need to import the dart:io package first, that is why config also has an import key specified.
Now for your local env, you can simply run
flutter pub run environment_config:generate
and it will generate the following Dart class for you:
Now you can import the Dart file anywhere in your code, and use the .env file with flutter_config package to pass specified values into your native code. And at the same time - you will have just one command to generate both!
If you want to have different values - just run a command with the appropriate key-value pairs like --ENV=prod --API_URL=http://example.com and you will get a configuration with these values. Overall, you can define any arguments based on specified fields in the YAML config.
Additional bonus: Since each field has reference to a global var, integration with a build tool like CodeMagic will be as simple as the following:
Define Environment variables
And then just add command run to the pre-build script section:
And that’s pretty much it. Now you can generate your environment specific configuration with just one small command and not expose your credentials anywhere except your build tool.
If you have any suggestions, please feel free to open an issue in environment_config. Likes and shares are appreciated! 🙂