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.dartto useAppConfig:
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.