← Back to posts Cover image for Mastering Multi-Environment Deployments in Flutter: Staging, Production & Beyond

Mastering Multi-Environment Deployments in Flutter: Staging, Production & Beyond

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

As your Flutter app grows from a simple prototype to a mission-critical application, managing different environments becomes essential. You need a safe playground (staging) to test new features without risking your live user data. Yet, many developers find themselves hard-coding API endpoints or toggling boolean flags, leading to messy code and deployment headaches. Let’s fix that.

The core problem is configuration entanglement. Your app needs to know which backend to talk to, which API keys to use, and which features to enable—all dependent on whether it’s a staging or production build. Mixing these configurations is a recipe for accidentally pointing your production app at a test database.

The Foundation: Flavors (or Schemes)

Flutter doesn’t have a built-in “environment” concept, but it provides a powerful mechanism through product flavors (on Android) and schemes (on iOS). These allow you to create distinct app variants from the same codebase, each with its own identifier and configuration. This is the cleanest way to achieve truly separate staging and production apps that can be installed side-by-side on a device.

Let’s set up two flavors: staging and production.

1. Android Setup (android/app/build.gradle): In the android block of your app/build.gradle, define your flavor dimensions and flavors.

android {
    ...
    flavorDimensions "environment"
    productFlavors {
        staging {
            dimension "environment"
            applicationIdSuffix ".staging"
            resValue "string", "app_name", "MyApp Staging"
        }
        production {
            dimension "environment"
            resValue "string", "app_name", "MyApp"
        }
    }
}

The applicationIdSuffix ensures the staging app gets a different package name (e.g., com.example.myapp.staging), allowing it to be installed alongside the production version (com.example.myapp).

2. iOS Setup (Xcode):

  • Open your iOS project in Xcode (ios/Runner.xcworkspace).
  • Select the Runner project in the project navigator.
  • Go to the Info tab. Under Configurations, duplicate the Debug and Release configurations for your staging environment (e.g., Debug-Staging, Release-Staging).
  • Go to the Build Settings tab. Search for “Product Bundle Identifier”. For your new Staging configurations, add a suffix (e.g., .staging) to the base identifier.
  • Finally, edit the scheme. Click on the current scheme name (next to the play button) > Edit Scheme… > Duplicate Scheme. Name it “Staging”. For each action (Run, Archive, etc.), change the Build Configuration to the corresponding -Staging configuration.

Injecting Configuration at Build Time

With flavors set up, we need to pass environment-specific variables into our Dart code. We’ll use Dart defines via the --dart-define flag during build.

Create a configuration class to hold these values:

// lib/core/config/app_config.dart
class AppConfig {
  final String appName;
  final String baseApiUrl;
  final bool isProduction;

  AppConfig({
    required this.appName,
    required this.baseApiUrl,
    required this.isProduction,
  });

  static AppConfig? _instance;

  static AppConfig get instance {
    if (_instance == null) {
      throw Exception('AppConfig has not been initialized. Call `AppConfig.initialize()` in main.dart');
    }
    return _instance!;
  }

  static void initialize({required String appName, required String baseApiUrl, required bool isProduction}) {
    _instance = AppConfig(
      appName: appName,
      baseApiUrl: baseApiUrl,
      isProduction: isProduction,
    );
  }
}

Now, in your main.dart, initialize this configuration before running the app:

// lib/main.dart
import 'package:flutter/material.dart';
import 'core/config/app_config.dart';

void main() {
  // These values will be passed from the command line during build.
  const appName = String.fromEnvironment('APP_NAME', defaultValue: 'MyApp');
  const baseApiUrl = String.fromEnvironment('BASE_API_URL', defaultValue: 'https://api.default.com');
  const isProduction = bool.fromEnvironment('IS_PRODUCTION', defaultValue: false);

  AppConfig.initialize(
    appName: appName,
    baseApiUrl: baseApiUrl,
    isProduction: isProduction,
  );

  runApp(const MyApp());
}

Building and Running with Flavors

You can now build your app with specific configurations:

# Build and run the staging app
flutter run --flavor staging --dart-define=APP_NAME="MyApp Staging" --dart-define=BASE_API_URL="https://staging-api.example.com" --dart-define=IS_PRODUCTION=false

# Build the production APK
flutter build apk --flavor production --dart-define=APP_NAME="MyApp" --dart-define=BASE_API_URL="https://api.example.com" --dart-define=IS_PRODUCTION=true

# Build the production IPA for iOS
flutter build ipa --flavor production --dart-define=APP_NAME="MyApp" --dart-define=BASE_API_URL="https://api.example.com" --dart-define=IS_PRODUCTION=true

Common Mistake Alert: Forgetting to set up the iOS schemes correctly is the number one cause of flavor issues on iOS. Double-check your Build Configurations in Xcode. Also, avoid using complex data types in --dart-define; stick to strings, booleans, and numbers.

Integrating with CI/CD

The real power unlocks in your CI/CD pipeline (e.g., GitHub Actions, Codemagic, Bitrise). Your pipeline scripts should contain the exact build commands for each environment.

A typical staging CI job might:

  1. Check out the code from the develop branch.
  2. Run tests.
  3. Execute the flutter build command with the staging flavor and corresponding --dart-define arguments.
  4. Upload the generated artifact to a testing service (like Firebase App Distribution).

The production job would:

  1. Check out the main branch.
  2. Run more extensive tests.
  3. Build with the production flavor.
  4. Upload to the Play Store Internal Track or TestFlight, or even promote to production.

Beyond Staging and Production

This pattern scales. You can add a demo flavor for sales, or a development flavor with verbose logging. Use the AppConfig class throughout your app to choose endpoints, enable/disable features, or change UI strings.

By separating your builds at the flavor level and injecting configuration, you create a robust, mistake-proof deployment process. Your staging app is truly isolated, and you can deploy both versions with confidence, knowing there’s no hidden connection between them. Start implementing flavors today—it’s a professional foundation for your app.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter Performance Deep Dive: Optimizing 'Vibe Coded' Apps for Speed and Responsiveness

Flutter Performance Deep Dive: Optimizing 'Vibe Coded' Apps for Speed and Responsiveness

Many developers start with 'vibe coding' for rapid prototyping, but this often leads to slow, unresponsive Flutter apps. This post will guide you through identifying performance bottlenecks in your Flutter projects, covering common culprits like unnecessary widget rebuilds, inefficient state management, and debugging differences between debug and release modes, to help you transform a 'vibe coded' app into a smooth, production-ready experience.

Cover image for Flutter & AI Code Generation: Beyond 'Vibe Coding' for Solo Developers

Flutter & AI Code Generation: Beyond 'Vibe Coding' for Solo Developers

AI code generation tools are rapidly evolving, but how can Flutter developers, especially solo founders, leverage them effectively without falling into 'vibe coding' pitfalls? This post will explore strategies for using AI to boost productivity, maintain code quality, and ensure architectural consistency in Flutter projects, addressing common concerns like context drift and code reuse.

Cover image for Flutter vs. KMP: Choosing the Right Cross-Platform Framework for Your Existing Native App

Flutter vs. KMP: Choosing the Right Cross-Platform Framework for Your Existing Native App

Many companies with existing native apps face the dilemma of choosing between Flutter and Kotlin Multiplatform (KMP) for cross-platform expansion. This post will provide a balanced comparison, discussing the pros and cons of each for teams with established native iOS (Obj-C) and Android (XML) codebases, especially concerning features like Bluetooth/WiFi connectivity, and guiding decision-making for long-term maintainability and developer experience.