DEV Community

Cover image for Setting Up Flavors in Flutter for Android and iOS
Taniksha Sharma
Taniksha Sharma

Posted on

Setting Up Flavors in Flutter for Android and iOS

Managing multiple environments in a Flutter app—like local, staging, and production—can be tricky if you don’t have a clean setup. In this blog, I’ll walk you through how I implemented flavors in my Flutter app, covering both Android and iOS, including MethodChannels, Xcode changes, and Flutter setup.

Why Flavors?
Flavors allow you to:

  • Keep separate configurations for local, staging, and production.
  • Use different API URLs, app names, and assets for each environment.
  • Avoid accidentally hitting production APIs during development.
  • Customize app behavior per environment (e.g., showing banners for staging/local builds).

Android Setup
For Android, I used product flavors in app/build.gradle. Here’s a simplified setup:

android {
    ...

    flavorDimensions "default"

    productFlavors {
        create("local") {
            dimension = "default"
            applicationId = "com.example.app"
            resValue("string", "app_name", "App Local")
            resValue("string", "flavor_name", "local")
            resValue("string", "base_url", "https://local.app")
        }

        create("staging") {
            dimension = "default"
            applicationId = "com.example.app"
            resValue("string", "app_name", "App Staging")
            resValue("string", "flavor_name", "staging")
            resValue("string", "base_url", "https://stg.app.com")
        }

        create("production") {
            dimension = "default"
            applicationId = "com.example.app"
            resValue("string", "app_name", "AppName")
            resValue("string", "flavor_name", "production")
            resValue("string", "base_url", "https://prod.app.com")
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Reading Flavors in Kotlin

In your MainActivity.kt, you can read flavor-specific values using a MethodChannel:

class MainActivity : FlutterActivity() {
    private val CHANNEL = "app_config"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "getFlavor" -> result.success(getString(R.string.flavor_name))
                    "getBaseUrl" -> result.success(getString(R.string.base_url))
                    else -> result.notImplemented()
                }
            }
    }
}

Enter fullscreen mode Exit fullscreen mode

iOS Setup

iOS requires a bit more configuration with schemes, configurations, and .xcconfig files.

Step 1: Create .xcconfig Files

Inside ios/Flutter, create three files:
local.xcconfig
staging.xcconfig
production.xcconfig

Example for production.xcconfig:

#include "Flutter/Generated.xcconfig"
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release-production.xcconfig"

APP_NAME = Jumpick
BASE_URL_SCHEME = https
BASE_URL_HOST = prod.app.com

Enter fullscreen mode Exit fullscreen mode

Similarly, configure local.xcconfig and staging.xcconfig with the respective names and URLs as we did for android.

Step 2: Info.plist Changes
In Info.plist, use placeholders for app name and base URL:

<key>CFBundleDisplayName</key>
<string>$(APP_NAME)</string>

<key>BASE_URL</key>
<string>$(BASE_URL_SCHEME)://$(BASE_URL_HOST)</string>

Enter fullscreen mode Exit fullscreen mode

Step 3: Create MethodChannel in Swift

In AppDelegate.swift, fetch the base URL for Flutter:

  // ✅ Create channel
       if let controller = window?.rootViewController as? FlutterViewController {
           let channel = FlutterMethodChannel(name: "app_config", binaryMessenger: controller.binaryMessenger)

           channel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
                   if call.method == "getBaseUrl" {
                     if let baseUrl = Bundle.main.object(forInfoDictionaryKey: "BASE_URL") as? String {
                       result(baseUrl)
                     } else {
                       result(nil)
                     }
                   } else {
                     result(FlutterMethodNotImplemented)
                   }
                 }
               }
Enter fullscreen mode Exit fullscreen mode

Step 4: Schemes and Configurations

  • Create new schemes for each flavor (Runner-local, Runner-staging, Runner-production).
  • Add Debug/Release configurations per flavor in Xcode.
  • Map each configuration to the corresponding .xcconfig file.

See below images for reference

flutter flavor xcode 1
flutter flavor xcode 2
Iflutter flavor xcode 3

Flutter Setup

Create a singleton AppConfig to fetch the base URL:

import 'dart:io';
import 'package:flutter/services.dart';

class AppConfig {
  static const _defaultBaseUrl = "https://prod.app.com";
  static const MethodChannel _channel = MethodChannel("app_config");
  static String? _cachedBaseUrl;

  static Future<String> get baseUrl async {
    if (_cachedBaseUrl != null) return _cachedBaseUrl!;
    if (Platform.isAndroid || Platform.isIOS) {
      try {
        _cachedBaseUrl = await _channel.invokeMethod<String>("getBaseUrl") ?? _defaultBaseUrl;
      } on PlatformException {
        _cachedBaseUrl = _defaultBaseUrl;
      }
    } else {
      _cachedBaseUrl = _defaultBaseUrl;
    }
    return _cachedBaseUrl!;
  }
}

Enter fullscreen mode Exit fullscreen mode

Show Environment Banner

In main.dart, show a small banner for local/staging:

 @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'Demo App',
      debugShowCheckedModeBanner: false,
      theme: lightTheme,
      darkTheme: darkTheme,
      themeMode: themeController.theme,
      initialRoute: AppPages.initial,
      getPages: AppPages.routes,

      builder: (context, child) {
        final originalScaler = MediaQuery.of(context).textScaler;
        final clampedScaler = originalScaler.clamp(
          minScaleFactor: 1.0,
          maxScaleFactor: 1.4,
        );

        Widget content = MediaQuery(
          data: MediaQuery.of(context).copyWith(textScaler: clampedScaler),
          child: child!,
        );

        // Add banner only if staging
        if (ApiConstants.baseUrl.contains("stg")) {
          content = Banner(
            message: "STAGING",
            location: BannerLocation.topEnd,
            color: Colors.orange.withValues(alpha: 0.8),
            textStyle: const TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 12,
            ),
            child: content,
          );
        }
        else if (ApiConstants.baseUrl.contains("local")) {
          content = Banner(
            message: "LOCAL",
            location: BannerLocation.topEnd,
            color: Colors.green.withValues(alpha: 0.85),
            textStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
            child: content,
          );
        }


        return content;
      },
    );


  }
Enter fullscreen mode Exit fullscreen mode

This makes it visually clear which environment is running. See below images for reference.

flutter flavor banner 1
flutter flavor banner 2

To install your app through android studio or xcode refer below screenshots. For android create new configurations from top bar.

flutter flavor android studio config1
flutter flavor android studio config2

TO BUILD APP THROUGH COMMANDS:
ANDROID

flutter build appbundle \
                      --flavor staging \
                      -t lib/main.dart \
                      --release
Enter fullscreen mode Exit fullscreen mode

IOS

 xcodebuild clean -workspace Runner.xcworkspace \
                                   -scheme Runner-staging \
                                   -configuration Release-staging

                  # Archive the build with explicit overrides
                     xcodebuild -workspace Runner.xcworkspace \
                     -scheme Runner-staging \
                     -configuration Release-staging \
                     -archivePath build/Runner-staging.xcarchive \
                     DEVELOPMENT_TEAM="$DEV_TEAM_ID" \
                     CODE_SIGN_STYLE=Automatic \
                     archive
Enter fullscreen mode Exit fullscreen mode

Wrap Up

  • Setting up flavors properly saves a lot of headaches. With the above setup:
  • Android flavors are easy via build.gradle.
  • iOS flavors are manageable with .xcconfig files and schemes.
  • Flutter can dynamically fetch the correct base URL or flavor.
  • You can display banners to avoid confusion during testing.
  • This setup makes switching environments seamless, helps with testing, and prevents accidental API calls to production during development.

Top comments (0)