Environment-Specific Configuration with Flutter

Last reviewed in November 2019 by James Dixon

Photo by Guillaume Bolduc on Unsplash

As you’re developing an app, you’ll inevitably need a different configuration depending on whether you’re developing locally or running in production. The canonical example is different API endpoints for local development versus production.

How can we do this in Flutter without having to change our configuration manually?

Create our configs

First, let’s create two JSON files to represent our local and production configurations:

// assets/config/dev.json
{
  "apiUrl": "http://localhost:3001"
}
// assets/config/prod.json
{
  "apiUrl": "https://api.myapp.com"
}

Next, let’s create a class that we can use to represent our config within our app:

// lib/app_config.dart
class AppConfig {
  final String apiUrl;
  
  AppConfig({this.apiUrl});
}

Great! We now have environment-specific configs and a method to access our configuration from within our app, but how do we actually load the JSON data into our AppConfig class?

If your config files contain sensitive information, don’t forget to add them to your .gitignore so they aren’t added to source control.

Loading our config

The easiest way load JSON data into your Flutter application is through the use of rootBundle.loadString().

First, we need to make our JSON files visible to rootBundle by adding the following to our pubspec.yaml:

flutter:
  assets:
    - assets/config/
rootBundle requires that your assets be located in the assets directory of your Flutter application, so be sure that you’ve created your config files in the assets/config/ directory.

Next, we’ll load our JSON file and convert the returned String to a Map so we can access the properties of our config individually:

// load the json file
final contents = await rootBundle.loadString(
  'assets/config/dev.json',
);

// decode our json
final json = jsonDecode(contents);

Now, let’s use our newly loaded config data to create an instance of our AppConfig class:

final config = AppConfig(apiUrl: json['apiUrl']);

Finally, we’ll wrap all of this up in a nice method on our AppConfig class called forEnvironment. This method accepts an environment (dev, prod, etc) and returns an instance of AppConfig representing the config for that environment.

// lib/app_config.dart
import 'dart:convert';
import 'package:flutter/services.dart';

class AppConfig {
  final String apiUrl;

  AppConfig({this.apiUrl});

  static Future<AppConfig> forEnvironment(String env) async {
    // set default to dev if nothing was passed
    env = env ?? 'dev';

    // load the json file
    final contents = await rootBundle.loadString(
      'assets/config/$env.json',
    );

    // decode our json
    final json = jsonDecode(contents);

    // convert our JSON into an instance of our AppConfig class
    return AppConfig(apiUrl: json['apiUrl']);
  }
}

Boom!

Now how the hell do we actually load the correct config into our app based on our build environment?

The best from the Flutter-verse in 3 minutes or less? Join Snacks!
Delivered twice monthly. No link walls. No spam. EVER.

Running your app with a specific config

If you’re coming from another language like Node or Java, you may have thought to set an environment variable or a command line flag and then use that to load the appropriate config. Unfortunately, with Flutter, you can’t pass any custom flags or compile time variables via the command line.

That leaves with a less sexy but workable option…

Multiple entry points…?

The multiple entry point approach takes advantage of the fact that we can tell the flutter run command which file we’d like to execute. By default, flutter run looks for lib/main.dart. Using the -t flag, we can change the target and execute a completely different file.

With this knowledge in hand, we can now create “entry points” that load the desired config based on our chosen build environment.

Putting it all together

First, let’s make the following modifications to our lib/main.dart file:

  • Allow main() to accept a named parameter called env that indicates the environment that we intend to build for.
  • Based on env, call the AppConfig.forEnvironment() method to load the proper JSON config file.
import 'package:env_specific_config/app_config.dart';
import 'package:flutter/material.dart';

void main({String env}) async {
  // load our config
  final config = await AppConfig.forEnvironment(env);

  // pass our config to our app
  runApp(MyApp(config));
}

With the above code, if we were to execute flutter run, it would load the dev config by default.

So how do we load another config from a different file…?

Create an entry point!

// main_prod.dart
import 'package:my_app/main.dart' as App;

void main() {
  // set config to prod
  App.main('prod');
}

We can now run our app with a production config by running the following command:

flutter run -t lib/main_prod.dart

Closing Thoughts

This is a highly trivialized example, but the approach will work both locally and even when using a CI/CD system, such as CodeMagic.

Here a couple of tips on things we didn’t cover in this article:

  • More than likely, you’ll have more than one field in your config file. That said, take a look at the json_serializable package to automatically generate the JSON parsing code for your AppConfig class.
  • Use InheritedWidget or packages like provider or get_it to make your AppConfig instance available anywhere in your application.
You can check out the code used in this example here.

The best from the Flutter-verse in 3 minutes or less? Join Snacks!

Delivered twice monthly. No link walls. No spam. EVER.