Configuring Different Build Flavors for Development, Staging, and Production in Flutter

In Flutter, build flavors are essential for managing different configurations of your application. They allow you to create distinct versions of your app, such as development, staging, and production builds, each with its own set of settings, assets, and functionalities. Configuring build flavors effectively ensures that your team can work efficiently, test thoroughly, and release with confidence. This post explores how to set up and use build flavors in Flutter for development, staging, and production environments.

Understanding Build Flavors in Flutter

Build flavors enable you to use different configurations in the same codebase. For instance, you might want to connect your development build to a mock API server while directing your production build to the live API. This is achieved through the use of command-line arguments and flavor-specific files.

Why Use Build Flavors?

  • Different API Endpoints: Use separate API URLs for development, staging, and production.
  • Feature Toggles: Enable or disable features based on the environment.
  • App Icons and Names: Use distinct app icons and names to differentiate builds on a device.
  • Environment-Specific Variables: Configure environment-specific variables without modifying the codebase.

Step-by-Step Guide to Configuring Build Flavors

Step 1: Project Setup

First, ensure you have a basic Flutter project set up. If not, create one using:

flutter create my_flutter_app

Step 2: Define Build Flavors in android/app/build.gradle

Open android/app/build.gradle and add flavorDimensions and productFlavors inside the android block.

android {
    // ...
    flavorDimensions "environment"

    productFlavors {
        development {
            dimension "environment"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
            buildConfigField "String", "API_URL", ""https://dev.example.com/api/""
            resValue "string", "app_name", "My Flutter App (Dev)"
        }
        staging {
            dimension "environment"
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
            buildConfigField "String", "API_URL", ""https://staging.example.com/api/""
            resValue "string", "app_name", "My Flutter App (Staging)"
        }
        production {
            dimension "environment"
            buildConfigField "String", "API_URL", ""https://example.com/api/""
            resValue "string", "app_name", "My Flutter App"
        }
    }

    // ...
}
  • flavorDimensions: Specifies the category of the build flavors.
  • productFlavors: Defines each flavor with its specific configurations.
    • applicationIdSuffix: Appends a suffix to the application ID for each flavor.
    • versionNameSuffix: Appends a suffix to the version name.
    • buildConfigField: Defines a constant value accessible in Dart code via BuildConfig.API_URL.
    • resValue: Defines a resource value accessible in XML resources.

Step 3: Create Different main.dart Entry Points

Create separate entry points for each environment to initialize flavor-specific configurations.

  • Create main_development.dart:
import 'package:flutter/material.dart';
import 'package:my_flutter_app/main.dart';
import 'package:my_flutter_app/config/app_config.dart';

void main() {
  AppConfig developmentConfig = AppConfig(
    appName: 'My Flutter App (Dev)',
    apiBaseUrl: 'https://dev.example.com/api/',
    flavor: 'development',
  );
  
  runApp(MyApp(appConfig: developmentConfig));
}
  • Create main_staging.dart:
import 'package:flutter/material.dart';
import 'package:my_flutter_app/main.dart';
import 'package:my_flutter_app/config/app_config.dart';

void main() {
  AppConfig stagingConfig = AppConfig(
    appName: 'My Flutter App (Staging)',
    apiBaseUrl: 'https://staging.example.com/api/',
    flavor: 'staging',
  );
  
  runApp(MyApp(appConfig: stagingConfig));
}
  • Create main_production.dart:
import 'package:flutter/material.dart';
import 'package:my_flutter_app/main.dart';
import 'package:my_flutter_app/config/app_config.dart';

void main() {
  AppConfig productionConfig = AppConfig(
    appName: 'My Flutter App',
    apiBaseUrl: 'https://example.com/api/',
    flavor: 'production',
  );
  
  runApp(MyApp(appConfig: productionConfig));
}

Step 4: Configure the Main App to Use Flavor Configuration

  • Create config/app_config.dart:
class AppConfig {
  final String appName;
  final String apiBaseUrl;
  final String flavor;

  AppConfig({
    required this.appName,
    required this.apiBaseUrl,
    required this.flavor,
  });
}
  • Update main.dart to use AppConfig:
import 'package:flutter/material.dart';
import 'package:my_flutter_app/config/app_config.dart';

void main() {
  // Fallback config in case no flavor is provided
  AppConfig defaultConfig = AppConfig(
    appName: 'My Flutter App',
    apiBaseUrl: 'https://example.com/api/',
    flavor: 'production',
  );
  
  runApp(MyApp(appConfig: defaultConfig));
}

class MyApp extends StatelessWidget {
  final AppConfig appConfig;

  const MyApp({Key? key, required this.appConfig}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: appConfig.appName,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: appConfig.appName, apiUrl: appConfig.apiBaseUrl),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;
  final String apiUrl;

  const MyHomePage({Key? key, required this.title, required this.apiUrl}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'API URL: $apiUrl',
              style: Theme.of(context).textTheme.headline6,
            ),
          ],
        ),
      ),
    );
  }
}

Step 5: Configure Launch Configurations in VS Code

To easily run each flavor, configure launch configurations in .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Flutter Dev",
            "type": "dart",
            "request": "launch",
            "program": "lib/main_development.dart",
            "args": [
                "--flavor",
                "development"
            ]
        },
        {
            "name": "Flutter Staging",
            "type": "dart",
            "request": "launch",
            "program": "lib/main_staging.dart",
            "args": [
                "--flavor",
                "staging"
            ]
        },
        {
            "name": "Flutter Production",
            "type": "dart",
            "request": "launch",
            "program": "lib/main_production.dart",
            "args": [
                "--flavor",
                "production"
            ]
        }
    ]
}

Step 6: Run the App with Build Flavors

Now, you can run each flavor using the command line or VS Code launch configurations.

  • Command Line:
flutter run --flavor development -t lib/main_development.dart
flutter run --flavor staging -t lib/main_staging.dart
flutter run --flavor production -t lib/main_production.dart
  • VS Code:

Select the desired configuration from the Run and Debug view and click “Start Debugging.”

Accessing Flavor-Specific Values

Access the AppConfig values in your widgets to customize behavior based on the flavor.

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final appConfig = context.findAncestorWidgetOfExactType()!.appConfig;

    return Text('App Name: ${appConfig.appName}, API URL: ${appConfig.apiBaseUrl}');
  }
}

Alternative Method using Conditional Compilation (Dart Defines)

You can also use Dart defines to control conditional compilation based on the flavor.

Step 1: Define Flavors in Command Line

When running the app, pass the flavor as a define:

flutter run --dart-define=FLAVOR=development -t lib/main.dart
flutter run --dart-define=FLAVOR=staging -t lib/main.dart
flutter run --dart-define=FLAVOR=production -t lib/main.dart

Step 2: Use Conditional Compilation

const flavor = String.fromEnvironment('FLAVOR', defaultValue: 'production');

void main() {
  String apiUrl;
  String appName;

  switch (flavor) {
    case 'development':
      apiUrl = 'https://dev.example.com/api/';
      appName = 'My Flutter App (Dev)';
      break;
    case 'staging':
      apiUrl = 'https://staging.example.com/api/';
      appName = 'My Flutter App (Staging)';
      break;
    default:
      apiUrl = 'https://example.com/api/';
      appName = 'My Flutter App';
  }

  AppConfig appConfig = AppConfig(
    appName: appName,
    apiBaseUrl: apiUrl,
    flavor: flavor,
  );

  runApp(MyApp(appConfig: appConfig));
}

Advantages and Disadvantages

Separate Entry Points:

  • Advantages:
    • Clear separation of concerns.
    • Easy to manage configurations for each flavor.
  • Disadvantages:
    • More boilerplate code.

Conditional Compilation:

  • Advantages:
    • Less boilerplate.
    • Single entry point.
  • Disadvantages:
    • Can become complex with many flavors.
    • Requires careful management of conditional logic.

Conclusion

Configuring different build flavors in Flutter for development, staging, and production environments is a crucial part of creating robust and maintainable applications. By setting up distinct flavors, you can easily manage different API endpoints, feature toggles, and app configurations. Whether you choose separate entry points or conditional compilation, the ability to differentiate your builds ensures a smoother development process and more reliable releases. Effectively utilizing build flavors is a key skill for any Flutter developer aiming to create high-quality applications.