Flutter 3.7 and a new way of defining compile-time variables

Denis Beketsky
ITNEXT
Published in
6 min readJan 28, 2023

--

Important Note: This method no longer allows to pass compile-time variables into the native layer with Flutter 3.19. Flutter team decided that it was a bug (per #130599 and #136444) and removed this funtionality. As for now this functionality wasn’t restored even after some extra discussion that can be found here.

There was a separate issue created to add this functionality with a separate param. But it seems it wasn’t addressed before 3.19 release was rolled out. As a tmp workaround, you can still use Dart Defines approach, which I described in my prev articles. Hopefully, Flutter team will add this functionality back soon.

Hello there. As you may know, recently Flutter 3.7 released and it has quite a lot of different improvements and features.

But today I would like to talk about an improvement that may be less noticeable, but at the same time, it is quite important. About new approach to specify compile-time variables for your project, and it’s not dart-define, well it is but not quite. And it’s Awesome!

Before we move forward, let’s discuss Dart Defines first for a little bit. Dart Defines was introduced back in the 1.17 release. It allows the definition of variables for both Dart and native layers by passing them as arguments like this:

--dart-define=someVariable=someValue

While it was a cool improvement, it had a few flaws.

First, if you would like to pass multiple variables, your execution command could become quite long.

The second, and most complicated part was that you were forced to do some Gradle and shell scripting to parse them properly for Android and iOS. And flutter had a few breaking changes in stable releases since initial release when they were changing the encoding approach around values for dart defines. More about this you can read here

So what was changed in 3.7?

While the general approach remains the same, a big improvement was made around how you can define compile-time variables and pass them into the native layers. If before you needed to have something like this

--dart-define=someVariable=someValue --dart-define=otherVariable=otherValue --dart-define=thirdVariable=thirdValue

Now you can use… Flavors!

Sorry, bad joke) No flavors are involved here 😁

Starting with Flutter 3.7 you can create JSON file with all your variables and pass it like this

--dart-define-from-file=<use-define-config.json>

And Flutter will parse it and define each variable separately! Cool, right?

So how does it work and how is it different from regular Dart Defines?

If you are setting up your project from scratch, it’s actually quite easy to start using JSON files with compile-time variables. And if you are already using Dart Defines, you will just need to remove some extra logic from your iOS build process and Gradle.

Note: From this point forward I will be using the same basic example from the Dart Defines article, with the same code examples and variable names.

So let’s start with the file itself. It’s very straightforward. We will create a regular JSON file with the name “config.json” and the following content:

{
"DEFINEEXAMPLE_APP_NAME": "awesomeApp1",
"DEFINEEXAMPLE_APP_SUFFIX": ".dev"
}

The format or pattern of the key name can be completely custom, just be sure that any of your keys won’t conflict with built-in Flutter or system variable names.

I would also recommend adding this file to gitignore to not send env variables to your VCS.

So if we build our app with the command like this

flutter run --dart-define-from-file=config.json

Later on in the Dart code we will be able to get access to provided variables using Environment getters (same as before):

const APP_NAME = String.fromEnvironment('DEFINEEXAMPLE_APP_NAME');
const APP_SUFFIX = String.fromEnvironment('DEFINEEXAMPLE_APP_SUFFIX');

You can take a look at the example app on GitHub if you want to. Branch flutter-3.7 contains an app that works with Flutter 3.7

I put a detailed explanation of what this App does in the original article about Dart Defines, so I won’t spend time on it)

What about Android?

While for Dart code nothing actually changed, the biggest change happened for Native layers.

To start using these values in our Android app… we can simply use them 🤷🏻‍♂️

No extra configuration or parsing is needed anymore. But let’s make it a little bit harder)

Let’s assume we don’t want Android build to fail with errors like “<variable name> is not defined” each time we forget to provide our compile-time variables. To fix this we can provide some default values for them in the app/build.gradle file:

def dartEnvironmentVariables = [
DEFINEEXAMPLE_APP_NAME: project.hasProperty('DEFINEEXAMPLE_APP_NAME')
? DEFINEEXAMPLE_APP_NAME
: 'awesomeApp',
DEFINEEXAMPLE_APP_SUFFIX: project.hasProperty('DEFINEEXAMPLE_APP_SUFFIX')
? DEFINEEXAMPLE_APP_SUFFIX
: null
];

Now we can use them in our build config for example like this

defaultConfig {
applicationId "com.example.defineexample"
applicationIdSuffix dartEnvironmentVariables.DEFINEEXAMPLE_APP_SUFFIX
....
resValue "string", "app_name", dartEnvironmentVariables.DEFINEEXAMPLE_APP_NAME
}

And then, after we defined String resource, it can be used 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.

Now if we open our app settings, we may notice that the App name is equal to the value that we have in the JSON file

Android App info

As well as the package name

If you are migrating from regular Dart Defines you won’t need to parse variables anymore as it was shown here. And this code can be safely removed (unless you still want to use Dart defines in addition to JSON file).

Ok, and what about iOS? Pre-build actions, base64, and all of this?

Luckily no 😊 The iOS situation is the same and as simple as Android. 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 variables from the config file there:

DEFINEEXAMPLE_APP_NAME=awesomeApp1
DEFINEEXAMPLE_APP_SUFFIX=.dev

We also can provide some defaults if we want to. Let’s create a file Defineexample-defaults.xcconfig and provide some values for these variables

DEFINEEXAMPLE_APP_NAME=awesomeApp
DEFINEEXAMPLE_APP_SUFFIX=

And then just import this file into Release and Debug.xcconfig files, just be sure that it was imported before Generated.xcconfig

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

Now we can start using these variables in Info.plist file to specify Bundle Name and Bundle Identifier:

When the application will be installed on your device, you can notice that it has a name, that we defined in the config.json

Also as Package name.

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

In case you are migrating from Dart Defines, ensure that you have removed Pre-Build script from Schema, which originally was parsing and create the xcconfig file with dart defines.

Please note that same as before, iOS has limitations around values for variables in the xcconfig file. You can read more about it here

Short explanation: xcconfig files treat the sequence // as a comment delimiter. Everything that goes after this sequense will be ignored. This means you can’t pass values like “https://example.com” to your compile time variable, since variable will get only “https:” part assigned.

That’s pretty much it) Example project can be found here:

Example in master branch is built for Flutter 2.2, flutter-1.19 — for Flutter 1.19, andflutter-1.17 — for Flutter 1.17, flutter-3.7 — for Flutter 3.7

Thank you for your time and happy coding!

--

--