Using Different Build Configurations in Flutter

When developing Flutter applications, managing different environments and configurations can become a challenge. Different builds (e.g., development, staging, production) often require varying sets of parameters like API endpoints, feature flags, and logging levels. In Flutter, build configurations allow you to tailor your app for various deployment environments without altering the core codebase. This article dives into how you can effectively use different build configurations in Flutter to streamline your development workflow.

Understanding Build Configurations

Build configurations, also known as build flavors or schemes, provide a way to compile your application with different settings. This enables you to run and test different versions of your application without manually changing code. Key advantages include:

  • Environment Management: Easily switch between development, staging, and production environments.
  • Feature Toggling: Enable or disable features based on the environment.
  • API Endpoint Flexibility: Use different API endpoints for testing and live versions.
  • Improved Testing: Deploy test builds with specific logging or debug settings.

Setting Up Build Configurations in Flutter

Flutter uses flavors along with schemes and targets (on iOS) to achieve different build configurations. Let’s walk through setting this up for both Android and iOS platforms.

1. Configuring Android Build Flavors

Step 1: Modify build.gradle

Open your android/app/build.gradle file and locate the android block. Add the flavorDimensions and productFlavors blocks as shown:


android {
    ...
    flavorDimensions "environment"

    productFlavors {
        dev {
            dimension "environment"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
            buildConfigField "String", "API_URL", ""https://dev.example.com/api""
        }
        prod {
            dimension "environment"
            buildConfigField "String", "API_URL", ""https://example.com/api""
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    ...
}

Here’s what the configuration does:

  • flavorDimensions "environment": Defines the flavor dimension, grouping the flavors logically.
  • productFlavors: Defines the individual flavors (dev and prod in this case).
  • applicationIdSuffix and versionNameSuffix: Adds a suffix to the application ID and version name for clarity.
  • buildConfigField: Defines a build configuration field API_URL with different values for each flavor.
Step 2: Access the Build Configuration

In your Flutter code, access the API_URL using:


import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:your_app_name/build_config.dart';  // Add this import


void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Build Config',
      home: Scaffold(
        appBar: AppBar(
          title: Text('API URL'),
        ),
        body: Center(
          child: Text(BuildConfig.API_URL),  // Using the API_URL here
        ),
      ),
    );
  }
}

Note: If you’re unable to find `BuildConfig.API_URL`, regenerate the build configuration. You can do this by cleaning and rebuilding your project, or by syncing your project with Gradle files. A common resolution is to execute the following commands in your terminal:


flutter clean
flutter pub get
flutter build apk --flavor dev --target lib/main_dev.dart

Replace `dev` with your desired flavor and `lib/main_dev.dart` with the appropriate entry point if needed. If prompted by Android Studio, “Invalidate Caches / Restart…” to ensure all build configurations are properly recognized.

Step 3: Running the App

To run the app with a specific flavor, use the following command:


flutter run --flavor dev --target lib/main_dev.dart

Replace dev with the desired flavor and lib/main_dev.dart with the appropriate entry point if needed.

2. Configuring iOS Build Schemes

Step 1: Define Schemes in Xcode

Open your Flutter project’s ios/Runner.xcworkspace file in Xcode. Go to Product > Scheme > Edit Scheme...

Create new schemes by duplicating the existing “Runner” scheme. Name them something descriptive, like “Runner-Dev” and “Runner-Prod.”

Create New Scheme

Step 2: Set Preprocessor Definitions

For each scheme:

  • Select “Run” in the left panel.
  • Go to the “Arguments” tab.
  • In the “Arguments Passed On Launch” section, add --dart-define=ENVIRONMENT=dev for the “Runner-Dev” scheme and --dart-define=ENVIRONMENT=prod for the “Runner-Prod” scheme.

Preprocessor Definitions

Step 3: Access the Scheme in Flutter

In your Flutter code, retrieve the environment variable using String.fromEnvironment:


const String environment = String.fromEnvironment('ENVIRONMENT', defaultValue: 'prod');

void main() {
  String apiUrl;
  if (environment == 'dev') {
    apiUrl = 'https://dev.example.com/api';
  } else {
    apiUrl = 'https://example.com/api';
  }

  runApp(MyApp(apiUrl: apiUrl));
}

class MyApp extends StatelessWidget {
  final String apiUrl;

  MyApp({required this.apiUrl});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Build Config',
      home: Scaffold(
        appBar: AppBar(
          title: Text('API URL'),
        ),
        body: Center(
          child: Text('API URL: $apiUrl'),
        ),
      ),
    );
  }
}
Step 4: Run the App

Select the desired scheme in Xcode and run your app.

Using Command Line with iOS

You can also build for iOS from the command line using flutter build ios or flutter run, and passing the scheme as a parameter. You’ll typically use this approach in CI/CD scenarios. Here’s an example:


flutter run --flavor dev --target lib/main_dev.dart -t ios/Runner/lib/main_dev.dart

Advanced Configuration Techniques

1. Environment-Specific Assets

You may need different assets (images, configuration files) for different environments. Organize them into directories that correspond to your flavors and load them conditionally.


String getAssetPath(String assetName) {
  if (environment == 'dev') {
    return 'assets/dev/$assetName';
  } else {
    return 'assets/prod/$assetName';
  }
}

2. Feature Flags

Implement feature flags to enable or disable features based on the build configuration. This is useful for testing new features in a staging environment.


bool isFeatureEnabled() {
  return environment == 'dev'; // Enable feature only in the dev environment
}

3. Using .env Files

Consider using .env files to manage environment variables, especially sensitive information like API keys. Packages like flutter_dotenv simplify loading environment variables from a .env file.


dependencies:
  flutter_dotenv: ^5.1.0

import 'package:flutter_dotenv/flutter_dotenv.dart';

void main() async {
  await dotenv.load(fileName: ".env");
  String apiKey = dotenv.env['API_KEY'] ?? 'default_api_key';
  runApp(MyApp(apiKey: apiKey));
}

Conclusion

Effectively using different build configurations in Flutter enhances your app development workflow by making it easier to manage environments, toggle features, and use different API endpoints. By following the steps outlined in this guide, you can set up Android flavors and iOS schemes to tailor your app for development, staging, and production environments. Using environment variables and feature flags further streamlines this process, enabling a robust and efficient development cycle.