Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, is incredibly versatile. A common requirement in app development is the ability to configure different deployment targets (e.g., App Store, Google Play Store, and web) from the same codebase. This ensures that your app behaves differently or has unique configurations based on the platform it is deployed to. This blog post will explore the various ways to achieve this in Flutter.
Understanding Deployment Targets
Before diving into the technical details, let’s define our deployment targets:
- App Store (iOS): Applications deployed to Apple’s App Store.
- Google Play Store (Android): Applications deployed to the Google Play Store.
- Web (Browser): Applications deployed as web apps running in a browser.
Each deployment target might require different configurations such as:
- Different app icons and splash screens.
- Unique API endpoints for development, staging, and production environments.
- Platform-specific feature flags or settings.
- Custom build configurations (e.g., code signing identities for iOS).
Methods to Configure Different Deployment Targets
1. Using Flutter Flavors
Flutter Flavors (also known as build variants) allow you to build different versions of your app from the same codebase. This is achieved through build configurations in Gradle (for Android) and Xcode (for iOS).
Step 1: Setting up Flavors in Android
Modify your android/app/build.gradle file to define product flavors:
android {
...
flavorDimensions "default"
productFlavors {
dev {
dimension "default"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
resValue "string", "app_name", "MyApp Dev"
buildConfigField "String", "API_URL", ""https://dev.example.com/api/""
}
prod {
dimension "default"
resValue "string", "app_name", "MyApp"
buildConfigField "String", "API_URL", ""https://example.com/api/""
}
}
...
}
In this configuration:
- We define two flavors:
devandprod. applicationIdSuffixandversionNameSuffixmodify the application ID and version name for thedevflavor.resValueallows you to override resources likeapp_name.buildConfigFielddefines build configuration fields likeAPI_URLthat can be accessed in your Dart code.
Step 2: Setting up Schemes in iOS
In Xcode, go to Product > Scheme > Edit Scheme. Duplicate the existing scheme and configure it for your different environments.

Add configurations for each flavor using build configurations. Each scheme will point to different .xcconfig files.
// ios/Flutter/dev.xcconfig
#include "Generated.xcconfig"
APP_DISPLAY_NAME = MyApp Dev
API_URL = https://dev.example.com/api/
// ios/Flutter/prod.xcconfig
#include "Generated.xcconfig"
APP_DISPLAY_NAME = MyApp
API_URL = https://example.com/api/
Step 3: Accessing Flavor-Specific Variables in Flutter
For Android, access the API_URL using the BuildConfig class:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
String apiUrl = const String.fromEnvironment('API_URL', defaultValue: 'https://default.example.com/api/');
if (kDebugMode) {
print('API URL: $apiUrl');
}
runApp(MyApp(apiUrl: apiUrl));
}
class MyApp extends StatelessWidget {
final String apiUrl;
const MyApp({Key? key, required this.apiUrl}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Flavors Example'),
),
body: Center(
child: Text('API URL: $apiUrl'),
),
),
);
}
}
For iOS, access variables defined in .xcconfig files:
import 'package:flutter/services.dart';
Future getApiUrl() async {
const MethodChannel methodChannel = MethodChannel('flavorConfig');
try {
final String result = await methodChannel.invokeMethod('getApiUrl');
return result;
} on PlatformException catch (e) {
print("Failed to get API URL: '${e.message}'.");
return 'https://default.example.com/api/';
}
}
And the corresponding platform-specific code (AppDelegate.swift for iOS):
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let flavorChannel = FlutterMethodChannel(name: "flavorConfig",
binaryMessenger: controller.binaryMessenger)
flavorChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "getApiUrl" {
result(Bundle.main.infoDictionary?["ApiUrl"] as? String)
} else {
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Step 4: Building Flavored Apps
To build the flavored apps:
- For Android:
flutter build apk --flavor devorflutter build apk --flavor prod - For iOS: Select the correct scheme in Xcode and build the app.
2. Using Environment Variables
Another way to configure different targets is by using environment variables. This method is useful for setting dynamic configurations without modifying the code.
Step 1: Set Environment Variables
Set environment variables specific to each environment.
For Linux/macOS:
export API_URL=https://dev.example.com/api/
For Windows:
$env:API_URL="https://dev.example.com/api/"
Step 2: Accessing Environment Variables in Flutter
Use the dart:io package to access these variables:
import 'dart:io';
void main() {
String apiUrl = Platform.environment['API_URL'] ?? 'https://default.example.com/api/';
print('API URL: $apiUrl');
runApp(MyApp(apiUrl: apiUrl));
}
Step 3: Deployment
Make sure to configure these variables on your deployment server or environment. For example, in a CI/CD pipeline or web server configuration.
3. Using Separate Configuration Files
A more structured approach is to use separate configuration files (e.g., config.dev.json, config.prod.json) for different environments.
Step 1: Create Configuration Files
Create different JSON configuration files for each environment.
// config.dev.json
{
"api_url": "https://dev.example.com/api/",
"app_name": "MyApp Dev"
}
// config.prod.json
{
"api_url": "https://example.com/api/",
"app_name": "MyApp"
}
Step 2: Load Configuration Files
Load these files during app initialization based on the deployment environment.
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
class AppConfig {
final String apiUrl;
final String appName;
AppConfig({required this.apiUrl, required this.appName});
static Future fromEnvironment(String environment) async {
try {
final String jsonString = await rootBundle.loadString('config.$environment.json');
final Map configData = json.decode(jsonString) as Map;
return AppConfig(
apiUrl: configData['api_url'] as String,
appName: configData['app_name'] as String,
);
} catch (e) {
print('Error loading config: $e');
return AppConfig(
apiUrl: 'https://default.example.com/api/',
appName: 'Default App',
);
}
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
const environment = String.fromEnvironment('APP_ENVIRONMENT', defaultValue: 'dev');
final config = await AppConfig.fromEnvironment(environment);
runApp(MyApp(config: config));
}
class MyApp extends StatelessWidget {
final AppConfig config;
const MyApp({Key? key, required this.config}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: config.appName,
home: Scaffold(
appBar: AppBar(
title: Text(config.appName),
),
body: Center(
child: Text('API URL: ${config.apiUrl}'),
),
),
);
}
}
Step 3: Building and Deploying
When building, set the APP_ENVIRONMENT variable to choose the correct configuration.
flutter run --dart-define=APP_ENVIRONMENT=prod
Conclusion
Configuring different deployment targets in Flutter can be achieved using various methods like Flutter Flavors, Environment Variables, and separate Configuration Files. Flutter Flavors are powerful for building platform-specific apps with unique settings managed through Gradle and Xcode. Environment Variables provide a dynamic way to set configurations during runtime, and Separate Configuration Files offer a structured method using JSON files loaded during app initialization.
Choose the method that best suits your project’s needs and complexity. Properly configured deployment targets ensure that your Flutter app behaves as expected across all platforms and environments, streamlining development, testing, and deployment processes.