Separate environments for your app using the same code base using Flutter Flavor
Tags: flutter, mobile, tutorial, programming
Published: 2024-04-04 05:57:49

> In an era of so much rapid development in this field of technology, efficiency and speed in the development of a mobile application are very important. In this article, I will discuss the flavor of Flutter that makes mobile application development more simple and seamless, as well as the solution to the problem of efficiency and speed in the development of mobile applications that we are facing today.

---

## 🔍  Navigating the Challenges

Imagine you, as a mobile developer, are asked to create a complex application. You are asked for your application to have several versions for several target users. For example, in an application that already exists in the Play Store and App Store, you are required to create three applications: applications that are used for global users, applications used for testers, and the last application used for developers. 

If you create the three applications manually, imagine how tricky and time-consuming the development process is. You need to customize the UI, change the source of your data, and build it one by one for every required application. In some cases, you may even need more than three of the same applications in different environments. 

Here's the flavor role that can help simplify and shorten the problem that was discussed earlier: With Flutter Flavor, you just need to create one source code; all you need to do is just configure whatever environment you're going to make and whatever content or assets that you'll make dynamic on each application in a different environment. So implementing Flutter Flavor will not only speed up and simplify the development process, but it will also reduce the risk of errors that will occur if you separate ingredients manually.

---

## 🔧  How it works?

In this current case, we will try to differentiate the application name, application logo, and base URL API for every application variant. This configuration will later be encapsulated in every flavor file you create, which will then be dynamically accessible to your source code in real time.
To illustrate how Flutter Flavor works, first we need to name our three applications as more clear and understandable environments by engineers; usually, these three environments will be separated into "production, staging, and development.”

- **Production**
This environment will be used for the entire final user that will be using your application, which will be on the Play Store and App Store.

- **Staging** 
This environment will be used for application testing that also reflects production data without interfering with original production data.

- **Development** 
This environment will be used for developers, where developers develop applications and also experiment.

The way it works is that you need to define the three environments described; each environment needs a configuration file that contains contents that you’re going to create dynamically (in this case, the name of the application, the application's logo, and the API base URL). Then you have to do the configuration on each native platform (Android and iOS), so that the native platform that you build will later know the three platforms that you have created. 

After making the configurations on the native platform, you also need to configure the IDE that you use (Android Studio or Visual Studio Code) so the IDE knows every environment you’ve created, which later will make it easier for you when running or debugging your applications (you just need to choose the environment that you’re going to run).

---

## 💻  Implementation
Let's demonstrate the implementation of flavors in a Flutter project using the example outlined above:

**Step 1: Create Flavor Configuration Files**

`flavor_config.dart`
```dart
import 'package:flutter/material.dart';

// Define an enum for every environment you want to create.
enum Flavor { prod, staging, dev }

// Add more values as needed.
class FlavorValues {
  final String apiBaseUrl;
  final String appIcon;
  final String appName;

  FlavorValues({
    required this.apiBaseUrl,
    required this.appIcon,
    required this.appName,
  });
}

class FlavorConfig {
  final Flavor flavor;
  final String name;
  final Color color;
  final FlavorValues values;
  static late FlavorConfig _instance;

  factory FlavorConfig({
    required Flavor flavor,
    // Color is an optional value that will be used for the flavor banner.
    // Flavor banner is one way to identify the environment you used.
    // You can remove this line of code if you don't need it.
    Color color = Colors.blue,
    required FlavorValues values,
  }) {
    _instance = FlavorConfig._internal(flavor, flavor.name, color, values);
    return _instance;
  }

  FlavorConfig._internal(this.flavor, this.name, this.color, this.values);

  static FlavorConfig get instance {
    return _instance;
  }

  // Method to check your current environment in your realtime code
  static bool isDevelopment() => _instance.flavor == Flavor.dev;

  static bool isStaging() => _instance.flavor == Flavor.staging;

  static bool isProduction() => _instance.flavor == Flavor.prod;
```

---

**Step 1.5 (Optional): Create Flavor Banner Configuration Files**

`flavor_banner.dart`
```dart
import 'dart:io';

import 'package:flutter/material.dart';

import 'flavor_config.dart';

const double _kHeightBanner = 12.0;
const double _kOffsetBanner = 40.0;
final double _kSizeTriangle =
    _kOffsetBanner + 0.707 * _kHeightBanner * (Platform.isIOS ? 2 : 3);

class FlavorBanner extends StatelessWidget {
  final Widget child;
  final BannerConfig? bannerConfig;

  const FlavorBanner({
    Key? key,
    required this.child,
    this.bannerConfig,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // You need to make sure your flavor banner doesn't show up in production.
    // If you want to show the banner in production, delete this line below.
    if (FlavorConfig.isProduction()) return child;
    BannerConfig config = bannerConfig ?? _getDefaultBanner();

    return Stack(
      children: <Widget>[
        child,
        _buildBanner(
          context: context,
          bannerConfig: config,
        ),
      ],
    );
  }

  BannerConfig _getDefaultBanner() {
    return BannerConfig(
      bannerName: FlavorConfig.instance.name,
      bannerColor: FlavorConfig.instance.color,
    );
  }

  // Creating banner allies so the banner will not be too long and overlap the UI.
  String getBannerAlias(String bannerName) {
    switch (bannerName) {
      case "production":
        return "PROD";

      case "stage":
        return "STAGE";

      case "dev":
        return "DEV";

      default:
        return "";
    }
  }

  Widget _buildBanner({
    required BuildContext context,
    required BannerConfig bannerConfig,
    BannerLocation location = BannerLocation.topStart,
  }) {
    return CustomPaint(
      painter: BannerPainter(
        message: getBannerAlias(bannerConfig.bannerName),
        textDirection: TextDirection.ltr,
        layoutDirection: TextDirection.ltr,
        location: location,
        color: bannerConfig.bannerColor,
      ),
      child: ClipPath(
        clipper: _TriangleClipper(location: location),
        child: Container(
          width: _kSizeTriangle,
          height: _kSizeTriangle,
          color: Colors.transparent,
        ),
      ),
    );
  }
}

class BannerConfig {
  final String bannerName;
  final Color bannerColor;

  BannerConfig({required this.bannerName, required this.bannerColor});
}

class _TriangleClipper extends CustomClipper<Path> {
  _TriangleClipper({
    required this.location,
  });

  BannerLocation location;

  @override
  Path getClip(Size size) {
    final path = Path();
    if (location == BannerLocation.topStart) {
      path.lineTo(size.width, 0);
      path.lineTo(size.width, Platform.isIOS ? 0 : size.height / 4);
      path.lineTo(0, size.height);
      path.lineTo(0, 0);
    }
    if (location == BannerLocation.topEnd) {
      path.lineTo(size.width, 0);
      path.lineTo(size.width, size.height);
      path.lineTo(0, Platform.isIOS ? 0 : size.height / 4);
      path.lineTo(0, 0);
    }
    if (location == BannerLocation.bottomStart) {
      path.lineTo(0, 0);
      path.lineTo(size.width, size.height);
      path.lineTo(0, size.height);
      path.lineTo(0, 0);
    }
    if (location == BannerLocation.bottomEnd) {
      path.lineTo(size.width, 0);
      path.lineTo(size.width, size.height);
      path.lineTo(0, size.height);
      path.lineTo(size.width, 0);
    }
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) {
    return false;
  }
}
```

---

**Step 2: Create a mains file for every environment you’ve created**

`main_prod.dart`
```dart
import 'main.dart';
import 'utils/flavor_config.dart';

void main() async {
  FlavorConfig(
    flavor: Flavor.prod,
    values: FlavorValues(
      apiBaseUrl: "https://apibaseurl-prod.com",
      appIcon: "assets/images/app_icon_prod.png",
      appName: "Application Production",
    ),
  );

  // Call your main method in here
  initializeApp();
}
```

`main_staging.dart`
```dart
import 'utils/flavor_config.dart';
import 'main.dart';

void main() async {
  FlavorConfig(
    flavor: Flavor.staging,
    values: FlavorValues(
      apiBaseUrl: "https://apibaseurl-staging.com",
      appIcon: "assets/images/app_icon_staging.png",
      appName: "Application Staging",
    ),
  );

  // Call your main method in here
  initializeApp();
}
```

`main_dev.dart`
```dart
import 'utils/flavor_config.dart';
import 'main.dart';

void main() async {
  FlavorConfig(
    flavor: Flavor.dev,
    values: FlavorValues(
      apiBaseUrl: "https://apibaseurl-dev.com",
      appIcon: "assets/images/app_icon_dev.png",
      appName: "Application Dev",
    ),
  );

  // Call your main method in here
  initializeApp();
}
```

and your original `main.dart` will looks like this
```dart
import 'package:flutter/material.dart';
import 'package:flutter_flavor_implementation_example/presentations/home_page.dart';
import 'package:flutter_flavor_implementation_example/utils/flavor_banner.dart';
import 'package:flutter_flavor_implementation_example/utils/flavor_config.dart';

void initializeApp() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: FlavorConfig.instance.values.appName,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      builder: (_, child) {
        return FlavorBanner(
          child: child ?? const SizedBox(),
        );
      },
      home: HomePage(title: FlavorConfig.instance.values.appName),
    );
  }
}
```

and your `home_page.dart` will looks like this
```dart
import 'package:flutter/material.dart';
import 'package:flutter_flavor_implementation_example/utils/flavor_config.dart';

class HomePage extends StatefulWidget {
  final String title;

  const HomePage({super.key, required this.title});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You currently running the app using ${FlavorConfig.instance.name} env',
            ),
            Text(
              "\nYour current environment base url is \n${FlavorConfig.instance.values.apiBaseUrl}",
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}
```

Make sure, after following all the steps above, you have all the required files and your project structure looks like this:
![Project Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hi8ile3j7xykvsd4l0er.png)

---

**Step 3: Setting up flavor in iOS by configuring the schemes**
In this step, you will configure iOS bundle schemes and bundle identifier
1. Open your project in Xcode (ios/Runner.xcworkspace).
2. In your `Runner` Project, add `-production` suffix in the current configuration. To rename the configuration, simply double-click the configuration and make sure it looks like this:
![Runner Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h08c1ow96sze5sh4abrq.png)
3. Now, you need to duplicate every configuration for every flavor you have by selecting the `Add (+)` icon and duplicating the configuration. Make sure you duplicate the correct configuration. In order to make `Debug-staging` you need to duplicate it from `Debug-production` not `Release-production` or `Profile-production` and it applies to all configurations.
![Duplicate Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8q2kcpkthd68mimz7bej.png)
After you duplicate all the configuration for every flavor, make sure your configuration looks like this:
![Final Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/606zof9szm4jwz0yxk4u.png)
4. Now in your `Runner` Targets, go to `Build Settings`, search for `Product Bundle Identifier`, and edit your bundle identifier by adding `.staging` and `.dev` suffix to your bundle identifier. We will exclude production because the production environment doesn't need the suffix.
![Bundle Identifier](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pcerq8fqtb1erdkod8mv.png)
5. After that, if you wanted to make your application name different between every flavor, you needed to go to `Runner` Targets, then go to `Build Settings`, and find `Product Name`. Change it to your desired application name.
![Product Name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9kvm3qiyjp85wreip2k0.png)
6. After you change your `Product Name`, to apply the changes to your app name, go to your `Info.plist`. (`Runner` > `Runner` > `Info`). Update the `Bundle Display Name` value to `$(PRODUCT_NAME)`.
![Bundle Display Name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/engzq5242u9szi1nxvsf.png)
7. Now, you need to rename your current scheme to production and also create two more schemes (staging and dev). You can rename your current scheme by opening `Product` > `Scheme` > `Manage Schemes` from the menu
![Manage Schemes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7bdg8vvbjeuri65bs54k.png)
And then duplicate your `Runner` scheme.
![Duplicate Scheme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tyn3mri1t64pvt8svpoo.png)
Make sure you name it `production` with lowercase because it is case-sensitive.
![Production Scheme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/16av48yti2j98xp4zm9s.png)
After you duplicate your current scheme, you can safely delete it by using the `Remove (-)` icon in the `left-bottom`
![Remove Runner Scheme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d9i3wfdjw7v34wspvmup.png)
8. Now, for the `staging` and `dev` you can create a new scheme by selecting `Product` > `Scheme` > `New Scheme` from the menu. Make sure it targets the `Runner`, and is also named with lowercase.
![Staging Scheme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ygbic7cfko9ulpnrohng.png)
![Dev Scheme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bvpnwxsvzo37o2t4m5j.png)
9. After that, you need to manage your `staging` and `dev` schemes so they target the correct configuration. To do that, you need to go to `Product` > `Scheme` > `Manage Schemes` from the menu.
![Manage Schemes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z16m8clg9i6c4vvvh5fb.png)
Select the `staging` scheme, and select the `Build Configuration` to correct the configuration you created. For example, in `Run` Menu, you need to change the `Build Configuration` to `Debug-staging`, and then in the `Profile` Menu, you need to change it to `Profile-staging` and apply it to all the menus.
![Staging Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pb6maa31qqtnoh0nvun8.png)
Do the same with the `dev` scheme.
![Dev Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cvzprq1xjwc8slw8zt0x.png)

---

**Step 4: Setting up flavor in Android by configuring the bundle**
1. Inside your Flutter project, navigate to `android/app/build.gradle`.
2. Create a `flavorDimension` to group your added product flavors. `Gradle` doesn’t combine product flavors that share the same `dimension`.
3. Add a `productFlavors` object with the desired flavors along with values for `dimension`, `resValue`, and `applicationId` or `applicationIdSuffix`.
```gradle
defaultConfig {
  ...
}

buildTypes {
  release {
    ...
  }
}

// Add these lines of code.
flavorDimensions "app"
productFlavors {
    production {
        dimension "app"
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        resValue "string", "app_name", "Flavor"
    }
    staging {
        applicationIdSuffix ".staging"
        dimension "app"
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        resValue "string", "app_name", "Flavor Staging"
    }
    dev {
        applicationIdSuffix ".dev"
        dimension "app"
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
        resValue "string", "app_name", "Flavor Dev"
    }
}
```
Then, in your `AndroidManifest.xml` update your `android:label` to `@string/app_name`
```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.vincwestley.flutter_flavor_implementation_example">
   <application
        android:label="@string/app_name"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>
```

---

**Step 5: Setting up your IDE launch configurations**

**5.1: Android Studio with UI**
1. In your Android Studio `Menu Bar`, select `Edit Configuration`
![Android Studio Edit Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kgptbikzg97yrrxzzrw4.png)
2. Update your `main.dart` configuration to `main_prod.dart`, also change the `Dart entrypoint` to your `main_prod.dart` file location, and change the `Build flavor` value to `production`.
![Production Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17t5hapmnpauzek1d7bc.png)
3. For `staging`, duplicate your `main_prod.dart` configuration.
![Duplicate Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1c8ytnh4gk3j5aqndxer.png)
4. Update all the configuration to target `staging` values: `name`, `entrypoint`, and `build flavor`.
![Staging Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wo1np9tbagaft2hx768d.png)
5. Do the same for the `dev` configuration.
![Dev Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jcbfgukmpb8tcvrcxtxv.png)

---

**5.2: Android Studio Manually**
In the root directory of your project, add a folder called `.run`. Inside the `.run` folder, create a file named `main-prod.dart.run.xml`, `main-staging.dart.run.xml`, and `main-dev.dart.run.xml`.

`main-prod.dart.run.xml`
```xml
<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="main-prod.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
    <option name="buildFlavor" value="production" />
    <option name="filePath" value="$PROJECT_DIR$/lib/main_prod.dart" />
    <method v="2" />
  </configuration>
</component>
```

`main-staging.dart.run.xml`
```xml
<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="main-staging.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
    <option name="buildFlavor" value="staging" />
    <option name="filePath" value="$PROJECT_DIR$/lib/main_staging.dart" />
    <method v="2" />
  </configuration>
</component>
```

`main-dev.dart.run.xml`
```xml
<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="main-dev.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
    <option name="buildFlavor" value="dev" />
    <option name="filePath" value="$PROJECT_DIR$/lib/main_dev.dart" />
    <method v="2" />
  </configuration>
</component>
```

---

**5.3 Visual Studio Code**
In the root directory of your project, add a folder called `.vscode`, Inside the `.vscode` folder, create a file named `launch.json`. In the `launch.json` file, add a configuration object for each flavor. Each configuration has a `name`, `request`, `type`, `program`, and `args` key.
```json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "prod",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_prod.dart",
      "args": ["--flavor", "production", "--target", "lib/main_prod.dart" ]
    },
    {
      "name": "staging",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_staging.dart",
      "args": ["--flavor", "staging", "--target", "lib/main_staging.dart" ]
    },
    {
      "name": "dev",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_dev.dart",
      "args": ["--flavor", "dev", "--target", "lib/main_dev.dart" ]
    }
  ],
  "compounds": []
}
```
By following the steps above, you’ve implemented a Flutter flavor that will make it easier for us to do application management with multiple dynamic data points. It will also make it easier for us to do maintenance and provide flexibility as well as efficiency in the development process.

---

## 🔥  App Demo
You can check out the full source code here: [Github](https://github.com/vincwestley/flutter_flavor_implementation_example)

![Demo 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dt0i1mzbkx6uvgdg8fhc.jpg)

You will have three different apps for the singular code with different data and configurations.
![Demo 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ya1ksnmwptpvn73x152r.png)

---

## ⭐  Conclusion
Flutter flavor makes mobile application development more simple and seamless, as well as the solution to the problem of efficiency and speed in the complex development of mobile applications that need to implement multiple environments. Implementing Flutter Flavor will not only speed up and simplify the development process, but it will also reduce the risk of errors that will occur if you separate ingredients manually.

---

## 📄  References
- [Create flavors of a Flutter app](https://docs.flutter.dev/deployment/flavors)

---