← Back to Articles

Flutter Build Modes and Flavors: Managing Multiple App Environments

Flutter Build Modes and Flavors: Managing Multiple App Environments

Flutter Build Modes and Flavors: Managing Multiple App Environments

As your Flutter app grows from a simple prototype to a production-ready application, you'll likely need to manage different environments—development, staging, and production. Each environment might need different API endpoints, app identifiers, or feature flags. This is where Flutter's build modes and flavors come into play.

In this article, we'll explore how to configure build modes and flavors in Flutter, helping you create separate app variants for different environments while maintaining a single codebase. Whether you're building an app that needs to connect to different backends or you want separate free and paid versions, understanding flavors will make your development workflow much smoother.

Understanding Build Modes

Flutter comes with three built-in build modes that control how your app is compiled:

  • Debug mode: Used during development. Includes debugging information, hot reload, and assertions. It's optimized for fast compilation and development experience.
  • Profile mode: Used for performance testing. Includes performance profiling tools but removes debugging overhead. This mode is similar to release mode but with additional profiling capabilities.
  • Release mode: Used for production. Optimized for performance and size, with all debugging information removed.

You can build in different modes using:


flutter run --debug
flutter run --profile
flutter run --release

While build modes handle how your app is compiled, flavors let you create different variants of your app—like separate apps for development and production environments, or different versions for different clients.

Build Modes vs Flavors Debug Mode Development Profile Mode Performance Release Mode Production Dev Flavor Environment Staging Flavor Environment Prod Flavor Environment

What Are Flavors?

Flavors (also called build variants or schemes) allow you to create multiple versions of your app from a single codebase. Each flavor can have:

  • Different app identifiers (bundle IDs)
  • Different app names and icons
  • Different API endpoints
  • Different environment variables
  • Different feature flags

For example, you might have:

  • A dev flavor that connects to your development server
  • A staging flavor that connects to your staging server
  • A prod flavor that connects to your production server

All three can be installed on the same device simultaneously because they have different app identifiers.

Single Codebase, Multiple App Variants Single Codebase Flutter Project with FlavorConfig Dev App com.app.dev Staging App com.app.staging Prod App com.app api-dev.example.com api-staging.example.com api.example.com

Setting Up Flavors in Flutter

Setting up flavors requires configuration in both your Dart code and your platform-specific files (Android and iOS). Let's walk through the process step by step.

Step 1: Define Flavors in Your Dart Code

First, create a configuration file to manage your flavors. Create a new file called flavor_config.dart:


enum Flavor {
  dev,
  staging,
  prod,
}

class FlavorConfig {
  static Flavor? appFlavor;

  static String get appName {
    switch (appFlavor) {
      case Flavor.dev:
        return 'MyApp Dev';
      case Flavor.staging:
        return 'MyApp Staging';
      case Flavor.prod:
        return 'MyApp';
      default:
        return 'MyApp';
    }
  }

  static String get apiBaseUrl {
    switch (appFlavor) {
      case Flavor.dev:
        return 'https://api-dev.example.com';
      case Flavor.staging:
        return 'https://api-staging.example.com';
      case Flavor.prod:
        return 'https://api.example.com';
      default:
        return 'https://api-dev.example.com';
    }
  }

  static bool get enableLogging {
    switch (appFlavor) {
      case Flavor.dev:
      case Flavor.staging:
        return true;
      case Flavor.prod:
        return false;
      default:
        return true;
    }
  }
}

This configuration class centralizes all flavor-specific settings. You can access these values throughout your app using FlavorConfig.apiBaseUrl or FlavorConfig.appName.

Step 2: Update Your Main Function

Next, modify your main.dart file to initialize the flavor based on compile-time constants:


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

void main() {
  // This will be set from the build command
  FlavorConfig.appFlavor = Flavor.dev; // Default, will be overridden
  
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: FlavorConfig.appName,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

Step 3: Configure Android Flavors

For Android, you'll need to modify your android/app/build.gradle file. Open it and add flavor configurations in the android block:


android {
    compileSdkVersion 33

    defaultConfig {
        applicationId "com.example.myapp"
        minSdkVersion 21
        targetSdkVersion 33
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

    flavorDimensions "default"
    productFlavors {
        dev {
            dimension "default"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
            resValue "string", "app_name", "MyApp Dev"
        }
        staging {
            dimension "default"
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
            resValue "string", "app_name", "MyApp Staging"
        }
        prod {
            dimension "default"
            resValue "string", "app_name", "MyApp"
        }
    }
}

You'll also need to update your android/app/src/main/AndroidManifest.xml to use the app name from resources:



    ...

Now you can build Android apps with different flavors:


flutter build apk --flavor dev --target lib/main_dev.dart
flutter build apk --flavor staging --target lib/main_staging.dart
flutter build apk --flavor prod --target lib/main_prod.dart

Step 4: Create Flavor-Specific Main Files

To properly set flavors at compile time, create separate main files for each flavor. Create lib/main_dev.dart:


import 'package:flutter/material.dart';
import 'main.dart' as runner;
import 'flavor_config.dart';

void main() {
  FlavorConfig.appFlavor = Flavor.dev;
  runner.main();
}

Create lib/main_staging.dart:


import 'package:flutter/material.dart';
import 'main.dart' as runner;
import 'flavor_config.dart';

void main() {
  FlavorConfig.appFlavor = Flavor.staging;
  runner.main();
}

And lib/main_prod.dart:


import 'package:flutter/material.dart';
import 'main.dart' as runner;
import 'flavor_config.dart';

void main() {
  FlavorConfig.appFlavor = Flavor.prod;
  runner.main();
}

Step 5: Configure iOS Flavors

For iOS, flavors are implemented using schemes and build configurations. Open your project in Xcode and follow these steps:

  1. Select your project in the Project Navigator
  2. Go to the "Info" tab
  3. Under "Configurations", duplicate your Debug and Release configurations for each flavor (dev, staging, prod)
  4. Create schemes for each flavor

Alternatively, you can use a script to automate this process. Create a script file or use Flutter's build commands with the --flavor flag after configuring schemes in Xcode.

To build iOS apps with flavors:


flutter build ios --flavor dev --target lib/main_dev.dart
flutter build ios --flavor staging --target lib/main_staging.dart
flutter build ios --flavor prod --target lib/main_prod.dart

Using Flavors in Your App

Once your flavors are set up, you can use the configuration throughout your app. Here's an example of how to use different API endpoints based on the flavor:


class ApiService {
  final String baseUrl = FlavorConfig.apiBaseUrl;
  
  Future> fetchData() async {
    final response = await http.get(
      Uri.parse('$baseUrl/api/data'),
    );
    return json.decode(response.body);
  }
}

You can also conditionally enable features based on the flavor:


class FeatureFlags {
  static bool get enableExperimentalFeatures {
    return FlavorConfig.appFlavor == Flavor.dev ||
           FlavorConfig.appFlavor == Flavor.staging;
  }
  
  static bool get enableDebugMenu {
    return FlavorConfig.appFlavor == Flavor.dev;
  }
}

Running Apps with Flavors

During development, you can run your app with a specific flavor:


flutter run --flavor dev --target lib/main_dev.dart
flutter run --flavor staging --target lib/main_staging.dart
flutter run --flavor prod --target lib/main_prod.dart

This allows you to test different environments without manually changing configuration files. Each flavor will have its own app identifier, so you can install multiple flavors on the same device simultaneously.

Flavor Configuration Flow Build Command --flavor dev main_dev.dart Sets Flavor.dev build.gradle Android Config Xcode Schemes iOS Config FlavorConfig Centralized Settings App uses FlavorConfig.apiBaseUrl, appName, etc.

Best Practices

Here are some tips to make working with flavors smoother:

  • Keep configuration centralized: Use a single configuration class like FlavorConfig to manage all flavor-specific settings. This makes it easy to see what differs between flavors and update values in one place.
  • Use environment variables for sensitive data: Never hardcode API keys or secrets in your code. Use environment variables or secure storage solutions, and load them based on the flavor.
  • Document your flavors: Make sure your team knows what each flavor is for and how to use it. Add comments in your configuration files explaining the purpose of each flavor.
  • Test all flavors: Before releasing, make sure to test your app in all flavors to ensure everything works correctly in each environment.
  • Use CI/CD: Automate building and deploying different flavors using continuous integration. This reduces human error and speeds up your release process.

Common Use Cases

Flavors are particularly useful for:

  • Environment management: Separate development, staging, and production environments with different API endpoints and configurations.
  • White-label apps: Create different versions of your app for different clients, each with their own branding, colors, and features.
  • Free vs. Premium versions: Build separate free and paid versions of your app from the same codebase, enabling or disabling features based on the flavor.
  • Regional variants: Create different versions for different regions or markets, each with localized content and compliance requirements.

Debugging Flavors

When debugging flavor-specific issues, you can check which flavor is active at runtime:


void debugFlavorInfo() {
  if (FlavorConfig.enableLogging) {
    print('Current flavor: ${FlavorConfig.appFlavor}');
    print('API URL: ${FlavorConfig.apiBaseUrl}');
    print('App name: ${FlavorConfig.appName}');
  }
}

You can also add a debug banner or indicator in your UI to show which flavor is active during development:


Widget build(BuildContext context) {
  return MaterialApp(
    title: FlavorConfig.appName,
    builder: (context, child) {
      return Stack(
        children: [
          child!,
          if (FlavorConfig.appFlavor != Flavor.prod)
            Positioned(
              top: 0,
              right: 0,
              child: Container(
                padding: EdgeInsets.all(4),
                color: Colors.red,
                child: Text(
                  FlavorConfig.appFlavor?.name.toUpperCase() ?? 'DEV',
                  style: TextStyle(color: Colors.white, fontSize: 10),
                ),
              ),
            ),
        ],
      );
    },
    home: const HomePage(),
  );
}

Conclusion

Flutter build modes and flavors provide a powerful way to manage multiple app environments and variants from a single codebase. By setting up flavors correctly, you can streamline your development workflow, reduce configuration errors, and make it easier to test and deploy your app across different environments.

While the initial setup requires some configuration in both Dart and platform-specific files, the benefits are well worth it. Once configured, switching between flavors is as simple as adding a flag to your build command, and you can have multiple versions of your app installed simultaneously for testing.

Start with a simple two-flavor setup (dev and prod) and expand as needed. As your app grows, you'll find flavors becoming an essential part of your development toolkit.