Get in touch
Thank you
We will get back to you as soon as possible
.pdf, .docx, .odt, .rtf, .txt, .pptx (max size 5 MB)

23.6.2020

7 min read

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything

Just recently, Flutter 1.17 was released and it brings a lot of cool features. You can read more about it here. It comes with lots of performance improvements, new Widgets, and more. But, besides all that, this version of Flutter also got one small, but a very useful feature: compile-time variables. Yes, you heard me, starting from 1.17 you can build your Android and iOS application with predefined, compile-time variables that can be used in all layers of your applications — Dart, iOS, Android.

Update 28.05.2020: Added notes about *.xcconfig limitations in iOS paragraph.

Update 05.08.2020: Added code example that compatible with Flutter 1.20. More about changes in Flutter 1.20 you can read here.

Let’s take a look closer

If you are working with Flutter Web, you may know the Flutter Tool argument --dart-define. Primarily it was used to enable Skia for the Flutter Web like this --dart-define=FLUTTER_WEB_USE_SKIA=true . But what you may not know, is that starting from 1.17 you can pass multiple custom key/value pairs there.

For example, if you run the flutter app with the following arguments

flutter run --dart-define=SOME_VAR=SOME_VALUE --dart-define=OTHER_VAR=OTHER_VALUE

then you can get these values in your Dart code like this

class EnvironmentConfig {
  static const SOME_VAR = String.fromEnvironment('SOME_VAR');
  static const OTHER_VAR = String.fromEnvironment('OTHER_VAR');
}

And use this class as your Environment specific config all over the project.

This means that now you can get rid of packages like environment_config.

If you don’t know what this package does, you can read this article on how to configure Flutter environment in the proper way.

Can I use these variables in native?

Short answer — yes, you can use these variables in native too but… it’s a little bit trickier.

If you are curious how Flutter passes Dart Defines into the native layers, you can take a look at the Flutter Tools package. It treats Dart Defines in a slightly different way for each platform but, I will show you examples for iOS and Android builds.

Also, keep in mind that starting from Flutter 1.20, Flutter Tool changed the way in which format Dart Defines are passed to the native layer.

To do so, I created a Flutter project that defines the Application Name and Application Suffix ID for Android and iOS based on Dart Defines variables. master branch is compatible with Flutter 1.20, and flutter-1.17 — compatible with Flutter .1.17

Let’s explore this application

This app works with just two compile-time variables which are DEFINEEXAMPLE_APP_NAME and DEFINEEXAMPLE_APP_SUFFIX. I would recommend adding a prefix to every compile-time variable just to avoid conflicts with variables that Flutter uses. In this example, I’m using DEFINEEXAMPLE_*.

Dart code

The application is quite simple. If you open main.dart you will see that it defines EnvironmentConfig class, and prints it’s value.

class EnvironmentConfig {
  static const APP_NAME = String.fromEnvironment(
    'DEFINEEXAMPLE_APP_NAME',
    defaultValue: 'awesomeApp'
  );
  static const APP_SUFFIX = String.fromEnvironment(
      'DEFINEEXAMPLE_APP_SUFFIX'
  );
}

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-1

You can notice that I used awesomeApp as a default value for DEFINEEXAMPLE_APP_NAME. We’re going to use this default value in all layers, including native. As for APP_SUFFIX — we will get an actual Application ID (packageName) in the FutureBuilder Widget from package_info plugin. And if DEFINEEXAMPLE_APP_SUFFIX is defined, you will see the Application ID on your screen.

One important note: ensure that you are assigning environment variables to the _const_ fields. Currently, Flutter has an issue if a non-const variable was used.

When you run this application with a command like

flutter run --dart-define=DEFINEEXAMPLE_APP_NAME=awesomeApp1 --dart-define=DEFINEEXAMPLE_APP_SUFFIX=.dev

You should see next:

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-2

You can notice that it rendered awesomeApp1, which was passed from the arguments instead of the default awesomeApp. And com.example.defineexample.**dev** . Where .dev — defined Suffix.

Android configuration

Ok, so to pass compile-time variables into Android we will need to use Flavors.

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-3

No worries! No Flavors, no tons of iOS Schemes, no multiple entries, and no copy/paste… well, except for default values for different layers. ??‍♂

So, for Android, Flutter Tool passes all Dart Defines as a single string value, joined with a comma. In Flutter 1.17 in your Gradle, you will get a String value like: DEFINEEXAMPLE_APP_NAME=awesomeApp1,DEFINEEXAMPLE_APP_SUFFIX=.dev

And in Flutter 1.20 value will be like this: DEFINEEXAMPLE_APP_NAME%3Dawesome1,DEFINEEXAMPLE_APP_SUFFIX%3D.dev

The case here that starting from Flutter 1.19 Flutter Tool encodes URI symbols before Dart Defines are passed to the compiler, including = that is why instead of this symbol you see %3D in the Dart Define item.

So we will need to parse it before we can use each key-value pair. Full gradle code can be found here. Flutter passes Dart Defines in the project properties with dart-defines key. For Flutter 1.17 parse logic will look like this:

def dartEnvironmentVariables = [ 
    DEFINEEXAMPLE_APP_NAME: 'awesomeApp',
    DEFINEEXAMPLE_APP_SUFFIX: null 
]; 
if (project.hasProperty('dart-defines')) {
    dartEnvironmentVariables = dartEnvironmentVariables + project.property('dart-defines') 
      .split(',') 
      .collectEntries { entry -› 
        def pair = entry.split('=') 
        [(pair.first()): pair.last()] 
      } 
}

First, it defines defaults (lines 1–4), then, converts a string from dart-defines into Map and merges the result with defaults.

For Flutter 1.20 we will need additionally decode each item so we could properly split it on key-value pair (line 5):

if (project.hasProperty('dart-defines')) {
    dartEnvironmentVariables = dartEnvironmentVariables + project.property('dart-defines')
        .split(',')
        .collectEntries { entry ->
            def pair = URLDecoder.decode(entry).split('=')
            [(pair.first()): pair.last()]
        }
}

So now we can define resValue and applicationIdSuffix.

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-4

After we defined String resource it can be used for example in your android/app/src/main/AndroidManifest.xml to specify Application Name. But basically, you can use this set of values anywhere in your project.

And now if you take a look at your application on the Android device, you can notice that the application name was successfully defined:

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-5

Also as Package Name

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-6

iOS configuration

During iOS build, Flutter Tool creates a couple of files, including Generated.xcconfig and flutter_export_environment.sh. They can be found in ios/Flutter. Those files are in gitignore by default, so GitHub project won’t contain them. But after the build, you will find Dart Defines in those files, defined like this for Flutter 1.17:

<span id="4183" class="eq kv je dt ki b ir mm mn s mo" data-selectable-paragraph="">DART_DEFINES=DEFINEEXAMPLE_APP_NAME=awesomeApp1,DEFINEEXAMPLE_APP_SUFFIX=.dev</span>

And for Flutter 1.20 they will be defined with encoded values like it was in the Gradle:

<span id="0fbb" class="eq kv je dt ki b ir mm mn s mo" data-selectable-paragraph="">DART_DEFINES=DEFINEEXAMPLE_APP_NAME%3Dawesome1,DEFINEEXAMPLE_APP_SUFFIX%3D.dev</span>

Since this format can’t be used for plist, to define Application Name and Application ID Suffix, we need to define new variables, based on Dart Defines. So let’s start.

Important Note About *.xcconfig

iOS *.xcconfig has some limitations when it comes to variable values. You can read about it here or google for a probable solution. But basically, it treats the sequence // as a comment delimiter. This means that you won’t be able to specify API_URL for example like this https://example.com since everything after // will be ignored. In the same time, if you run your build with the following argument:

--dart-define=https://example.com

Flutter Tools will create flutter_export_environment.sh file with the following row:

...<br>export "DART_DEFINES=APP_URL=https://example.com"

Also as Generated.xcconfig with the following:

...<br>DART_DEFINES=APP_URL=https://example.com

So in your build Pre-Action (you will read about it in the next paragraph), you can try to figure out a workaround how to get actual value of DART_DEFINES to provide proper parsing of API_URL if you want to.

With Flutter 1.20 situation will be similar since we will decode values before parse, but since Dart Define item will be encoded (including // ) it will be easier to figure out a workaround how you want to parse such items. Because, unlike Flutter 1.17, you will actually receive your Dart Define value so you no need to think how to get this value for parse.


1. Create Defineexample-defaults.xcconfig file with defaults:

DEFINEEXAMPLE_APP_NAME=awesomeApp<br>DEFINEEXAMPLE_APP_SUFFIX=

2. Import it in Debug and Release configurations

And also let’s import config file that we will create in just a few moments:

#include "Generated.xcconfig"
#include "Defineexample-defaults.xcconfig"
#include "Defineexample.xcconfig"

Consider putting Defineexample.xcconfig after Defineexample-defaults.xcconfig, so values from Flutter Run command could override default values.

3. Edit your plist file and define Bundle name from **DEFINEEXAMPLE_APP_NAME** and Suffix from **DEFINEEXAMPLE_APP_SUFFIX**

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-7

Now it’s time to create Defineexample.xcconfig. To create it we need to add Pre-Build action to the iOS build.

4. Edit Schema:

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-8

5. Add Pre-Action:

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-9

with the following command if you use Flutter 1.17:

echo "$DART_DEFINES" | tr ',' '\n' › ${SRCROOT}/Flutter/Defineexample.xcconfig

Or the following command if you use Flutter 1.20:

# Type a script or drag a script file from your workspace to insert its path.

function urldecode() { : "${*//+/ }"; echo "${_//%/\\x}"; }

IFS=',' read -r -a define_items <<< "$DART_DEFINES"


for index in "${!define_items[@]}"
do
    define_items[$index]=$(urldecode "${define_items[$index]}");
done

printf "%s\n" "${define_items[@]}"|grep '^DEFINEEXAMPLE_' > ${SRCROOT}/Flutter/Defineexample.xcconfig

The explanation why exact this bash script is needed you can find in this article.

During the build, this command will create the following Defineexample.xcconfig file:

<span id="3689" class="jz iq ds as jj b fd ll lm r ln" data-selectable-paragraph="">DEFINEEXAMPLE_APP_NAME=awesomeApp1<br>DEFINEEXAMPLE_APP_SUFFIX=.dev</span>

You can choose different *.xcconfig file names if you want to. Just ensure that they won’t be modified by Flutter Tool scripts.

Also, keep in mind limitations of *.xcconfig if you need to respect // during the parse.

When the application will be installed on your device, you can notice that it has a name, that we defined during the build:

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-10

Also as Package Name:

Flutter 1.17 — no more Flavors, no more iOS Schemas. Command argument that changes everything-11

Same as with Android, now you can use this set of values anywhere in your project.

That’s pretty much it :) The example project can be found here. Example in master branch is built for Flutter 1.20, flutter-1.17 — for Flutter 1.17.

Thank you for your time and happy coding!

0 Comments
name *
email *