Build variants in Flutter for multiple backend environments

Ishaan Bahal
Meeve
Published in
6 min readDec 26, 2018

--

A couple of months ago, I started developing Meeve with Flutter. I’ve worked with android in the past and was used to having build variants (flavours) that could target different backends. Flutter didn’t come with any such thing and I had to figure out stuff myself. So, this post would be a small one, to help prepare your apps for actual releases and not playing around on a laptop and for teams that have testers that need different builds.

Photo by Mike Setchell on Unsplash

What are build variants and why would you need them?

A flavour or a build variant can be multiple things, it can be used to target different device architectures or different backends. Android presents a separate distinction to both, so you can target different Android API versions and well as different backend support, for iOS I’m not quite certain, but it also does support these in some fashion.

I needed a solution to build my app, and test on different backend environments without having to change constants every time! To do this, I needed a more dart based solution and not dependent on platforms. Now keeping it in dart also meant that I couldn’t use the features that platforms ship with and most certainly cannot use different build names on Android to deploy multiple apps on the same hardware. But this approach worked for me, and it will work for any target platform in future since its dart only and is just a hook before the main.dart process.

For people building with CI systems, this method might be helpful since a single command would make sure the correct backend values are being bundled into the code for every environment the CI system wants to create a build for.

Modifying main.dart to support variants

As I said, the process needed to be dart only, and the entry point to the app is main.dart, in fact while running or building, you usually do this

flutter build --apk --release

but you can also do this

flutter build --apk -t lib/main.dart --release

This method actually tells the build system to use lib/main.dart as the entry point for the flutter app, and this is what we’ll modify.

So, let’s say we need 3 environments, dev, staging and prod. Dev would be the local environment, staging would be a separate config of staging servers and prod would be the production config.

Let’s see an example of environment based build file

import 'package:build_amaze/constants/constants.dart';
import 'package:build_amaze/main.dart';

void main(){
Constants.setEnvironment(Environment.DEV);
mainDelegate();
}

This is main_dev.dart and similarly, main_staging.dart and main_prod.dart follow the same logic and just change the value to Environment.STAGING and Environment.PROD.The actual logic for environment selection and values lies with constants.dart, we’ll look at it in a bit, but first here’s a look at main.dart

import 'package:build_amaze/constants/constants.dart';
import 'package:flutter/material.dart';

void mainDelegate() => runApp(MyApp());

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Build env - ${Constants.WHERE_AM_I}',
theme: ThemeData(
primarySwatch: Colors.purple,
),
home: Scaffold(
appBar: AppBar(
title: Text('Build env - ${Constants.WHERE_AM_I}'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text('Build env - ${Constants.WHERE_AM_I}',),
Divider(),
Text("Server 1: ${Constants.SERVER_ONE}"),
Divider(),
Text("Server 2: ${Constants.SERVER_TWO}"),
Divider(),
Constants.WHERE_AM_I == "prod"
? Text("OMG! Such a prod!")
: Text("You're still testing man!"),
],
))),
debugShowCheckedModeBanner: false,
);
}
}

So the only change here is I renamed main() method to mainDelegate() so no one can accidentally run this and the exported method is used by all three of my environment based main files.

Now, let’s take a look at the constants.dart file.

enum Environment { DEV, STAGING, PROD }

class Constants {
static Map<String, dynamic> _config;

static void setEnvironment(Environment env) {
switch (env) {
case Environment.DEV:
_config = _Config.debugConstants;
break;
case Environment.STAGING:
_config = _Config.qaConstants;
break;
case Environment.PROD:
_config = _Config.prodConstants;
break;
}
}

static get SERVER_ONE {
return _config[_Config.SERVER_ONE];
}

static get SERVER_TWO {
return _config[_Config.SERVER_TWO];
}

static get WHERE_AM_I {
return _config[_Config.WHERE_AM_I];
}
}

class _Config {
static const SERVER_ONE = "SERVER_ONE";
static const SERVER_TWO = "SERVER_TWO";
static const WHERE_AM_I = "WHERE_AM_I";

static Map<String, dynamic> debugConstants = {
SERVER_ONE: "localhost:3000/",
SERVER_TWO: "localhost:8080/",
WHERE_AM_I: "local",
};

static Map<String, dynamic> qaConstants = {
SERVER_ONE: "https://staging1.example.com/",
SERVER_TWO: "https://staging2.example.com/",
WHERE_AM_I: "staging",
};

static Map<String, dynamic> prodConstants = {
SERVER_ONE: "https://itsallwidgets.com/",
SERVER_TWO: "https://flutter.io/",
WHERE_AM_I: "prod"
};
}

Now as you can see, for any value I need in my code, I have to create a getter in the constants method, and it fetches the value from the selected map that was decided during the build stage. It may seem a bit too much to keep writing all the getters and map values, but it is sort of convenient once setup properly.

Now let’s take a look at making this work correctly with our usual workflow in Android Studio (or IntelliJ).

Android Studio support for custom entry point

For android studio build support, just create multiple custom configurations by going to the edit configuration panel.

Edit Configuration in Android Studio

Then create a new flutter configuration by clicking the (+) icon on the top left and selecting flutter from there.

Configuration for the dev build

Create similar configurations for all the environments you have.

And now run any. It will work as it used to before, hot reload and everything and now you have support for multiple environments. You can also share the workspace preferences with other people so they do not have to set these up again.

I’ve not tried this with Visual Studio Code, but if it supports build configurations, then the process should be very similar to this one.

Note:

  • If you have shared preferences, or some stored media, or logged in accounts, just clear the app preferences or uninstall it before changing the environment.
  • Changing the environment will cause a complete rebuild and will cause the app to restart completely.
  • This also modifies the generated.xcconfig file that’s required for iOS release. So before you make a release with XCode, make sure you run flutter build -t lib/main_prod.dart --release at least once separately before archiving the build.

Tip: It’s always good to write a small build script that has all the final build steps in it. Mine contains the build commands for both Android and iOS, I run it once, and wait a while, collect the apk and then I start the archive process on XCode to upload to iTunes.

Here’s the app running using all three configs:

App running on different build environments

The above method is an amateur attempt at making multiple configs works. It borrows from concepts shared on

and builds upon them. The code may not be quite secure or well written, I’m not very good at dart. But this method does work and requires very little effort to make it so. :)

And, I’ve used a similar process to setup build for Meeve, the product that I work on and have been able to test and release the app on both platforms using multiple environments.

Checkout Meeve, a hyperlocal platform to help you discover people nearby through events, available on iOS and Android!

--

--