ποΈ Real-World Build Variants, Environments & CI/CD for Flutter Projects (Part 1)
FlutterPulseThis article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!π

A comprehensive guide to managing multiple environments, build configurations, and automated deployment pipelines in Flutter applications.
"The best time to set up proper CI/CD was yesterday. The second-best time is now." β Me, after debugging production issues at 2 AM for the third time in a week.
π Introduction
In serious mobile app development, managing environments and automating deployments aren't just best practices β they're essential. Whether you're releasing a hotfix to production, testing QA builds in staging, or debugging in development, robust build variants and CI/CD systems ensure quality and speed.
In this article, we'll explore real-world build variant structures, secure configuration practices, and a CI/CD pipeline powered by GitHub Actions tailored for Flutter teams.
π― The Problem That Started It All
π‘ What You'll Learn: Understanding why manual deployment processes fail and recognizing common deployment anti-patterns that hurt teams.
Picture this: It's Thursday evening, and we're preparing for a major app release. Our process looked something like this:
- Build the staging version manually
- Test it on a few devices
- Build the production version (fingers crossed we didn't mess up the config)
- Upload to app stores
- Pray everything works
Sound familiar? We were essentially playing Russian roulette with our deployments.
The breaking point came when we accidentally shipped our staging API endpoints to production. 50,000 users couldn't access their data for 6 hours. That's when I knew something had to change.
π¨ Common Deployment Horror Stories
- Shipping debug builds with console logs to production
- Using wrong API keys causing payment failures
- Deploying with test data visible to real users
- App crashes due to missing production certificates
π Bonus Tip: Create a deployment checklist and stick it on your monitor. Even with automation, human oversight matters!
π§© Understanding the Flutter Environment Puzzle
π‘ What You'll Learn: The fundamental concepts of environment separation in Flutter and why it's different from traditional Android/iOS development.
Before diving into solutions, let's get one thing straight: Flutter doesn't have "flavors" like Android does natively. We need to create our own system, and thankfully, Flutter gives us the tools to do it elegantly.
π― Why Multiple Environments Matter (Beyond the Obvious)
Sure, everyone knows about dev/staging/prod separation. But here's what I learned the hard way:

π§ Why Environment Separation Matters
- Isolation: Avoid accidental production damage during testing
- Staged Rollouts: Gradual feature release & testing
- Security: Prevent production credentials from leaking
- Configurability: Change endpoints, feature toggles, or logging behavior easily
- Different API Keys: Your analytics, crash reporting, and payment processors all need different configurations
- Feature Flags: Some features should only be available in certain environments
- App Icons: Users (and your QA team) need visual cues about which version they're using
- Debug Information: Development builds need extra logging that would be security risks in production
π Bonus Tip: Use different color schemes for each environment. Your QA team will thank you when they can instantly identify which build they're testing!
ποΈ Organizing Your Flutter Project for Environments
π Project Structure
lib/
βββ main.dart
βββ main_dev.dart
βββ main_staging.dart
βββ main_prod.dart
βββ config/
β βββ app_config.dart
β βββ dev_config.dart
β βββ staging_config.dart
β βββ prod_config.dart
ποΈ Building a Bulletproof Configuration System
π‘ What You'll Learn: How to create a scalable, maintainable configuration system that works across all environments without code duplication.
After researching various approaches and reading through Flutter's official documentation, I settled on a pattern that's served us well for over two years.
π― The Foundation: Abstract Configuration
Here's the approach that changed everything for us:
// config/app_config.dart
// π― This abstract class defines the contract for all environments
abstract class AppConfig {
static late AppConfig _instance;
static AppConfig get instance => _instance;
// π± App Identity
String get appName;
String get baseApiUrl;
String get environment;
bool get isDebugMode;
String get appSuffix;
// π Analytics & Monitoring
String get analyticsKey;
String get crashlyticsKey;
// π Feature Flags
bool get enableBetaFeatures;
bool get showDebugInfo;
// π§ Configuration Method
static void configure(AppConfig config) {
_instance = config;
}
}
π Code Breakdown for Beginners:
abstract class: This is like a template that other classes must followstatic late: This creates a single instance shared across the entire appget: These are getters that return values (like variables but computed)
π― Environment-Specific Implementations
The magic happens in the concrete implementations:
// config/environments/development_config.dart
// π΄ Development Configuration - For daily coding
class DevelopmentConfig extends AppConfig {
@override
String get appName => "MyApp (Dev)";
@override
String get baseApiUrl => "https://api-dev.myapp.com";
@override
String get environment => "development";
@override
bool get isDebugMode => true; // π Debug logs enabled
@override
String get appSuffix => ".dev"; // π± Different bundle ID
// π Development Analytics (won't pollute prod data)
@override
String get analyticsKey => "dev-analytics-key";
@override
String get crashlyticsKey => "dev-crashlytics-key";
// π All features enabled for testing
@override
bool get enableBetaFeatures => true;
@override
bool get showDebugInfo => true; // π Show debug overlays
}
// config/environments/production_config.dart
// π’ Production Configuration - For real users
class ProductionConfig extends AppConfig {
@override
String get appName => "MyApp";
@override
String get baseApiUrl => "https://api.myapp.com";
@override
String get environment => "production";
@override
bool get isDebugMode => false; // π No debug info
@override
String get appSuffix => ""; // π± Clean bundle ID
// π Production Analytics
@override
String get analyticsKey => const String.fromEnvironment(
'PROD_ANALYTICS_KEY',
defaultValue: '',
);
@override
String get crashlyticsKey => const String.fromEnvironment(
'PROD_CRASHLYTICS_KEY',
defaultValue: '',
);
// π Feature flags controlled remotely
@override
bool get enableBetaFeatures => false;
@override
bool get showDebugInfo => false; // π Never show debug info
}
π For Beginners:
@override: This means we're providing our own version of the abstract methodconst String.fromEnvironment(): This reads values from build-time environment variables- Different environments = different behaviors without changing code
π Pro Tip: Use const String.fromEnvironment() for sensitive data. It's read at compile-time, making it more secure than runtime configuration.
π Entry Point Example
// main_development.dart
// π΄ Development App Entry Point
import 'package:flutter/material.dart';
import 'config/environments/development_config.dart';
import 'app.dart';
void main() {
// π§ Configure for development
AppConfig.configure(DevelopmentConfig());
// π Launch the app
runApp(MyApp());
}
// main_production.dart
// π’ Production App Entry Point
import 'package:flutter/material.dart';
import 'config/environments/production_config.dart';
import 'app.dart';
void main() {
// π§ Configure for production
AppConfig.configure(ProductionConfig());
// π Launch the app
runApp(MyApp());
}
Use flutter build apk --flavor dev -t lib/main_dev.dart to build.
π Bonus Tip: Create a main_flavor_checker.dart that prints the current configuration on startup. Great for debugging configuration issues!
π± Platform-Specific Configuration: The Flutter Way
π‘ What You'll Learn: How to configure Android flavors and iOS schemes to work seamlessly with Flutter's build system, including bundle IDs, app names, and icons.
π€ Android Setup: Gradle Flavors
In android/app/build.gradle, we define our flavors:
// android/app/build.gradle
android {
compileSdkVersion flutter.compileSdkVersion
defaultConfig {
applicationId "com.example.myapp"
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
// π― Define flavor dimensions (categories)
flavorDimensions "environment"
// π― Product flavors for different environments
productFlavors {
development {
dimension "environment"
applicationIdSuffix ".dev" // π± com.example.myapp.dev
versionNameSuffix "-dev" // π 1.0.0-dev
resValue "string", "app_name", "MyApp Dev" // π± App name
// π¨ Custom app icon for development
// Place dev icons in: android/app/src/development/res/
}
staging {
dimension "environment"
applicationIdSuffix ".staging" // π± com.example.myapp.staging
versionNameSuffix "-staging" // π 1.0.0-staging
resValue "string", "app_name", "MyApp Staging"
// π¨ Custom app icon for staging
// Place staging icons in: android/app/src/staging/res/
}
production {
dimension "environment"
resValue "string", "app_name", "MyApp" // π± Clean app name
// π¨ Production app icon
// Use default icons in: android/app/src/main/res/
}
}
// π Signing configurations
signingConfigs {
release {
if (project.hasProperty('android.injected.signing.store.file')) {
storeFile file(project.property('android.injected.signing.store.file'))
storePassword project.property('android.injected.signing.store.password')
keyAlias project.property('android.injected.signing.key.alias')
keyPassword project.property('android.injected.signing.key.password')
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true // ποΈ Code shrinking
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
π For Beginners:
applicationIdSuffix: Adds text to your app's ID so you can install multiple versionsversionNameSuffix: Helps identify which version you're looking atresValue: Creates Android resources (like strings) at build timesigningConfig: How Android knows the app is really from you
π Folder Structure for Custom Icons:
android/app/src/
βββ main/res/ # π’ Production icons
β βββ mipmap-hdpi/
β βββ mipmap-mdpi/
β βββ ...
βββ development/res/ # π΄ Development icons (red tint)
β βββ mipmap-hdpi/
β βββ ...
βββ staging/res/ # π‘ Staging icons (orange tint)
βββ mipmap-hdpi/
βββ ...
π iOS Configuration: Xcode Schemes
For iOS, we create different schemes in Xcode:
π Step-by-Step Setup:
- Open Xcode β Open
ios/Runner.xcworkspace - Duplicate Scheme:
- Product β Scheme β Manage Schemes
- Select "Runner" β Duplicate (3 times)
- Rename to:
Runner-Dev,Runner-Staging,Runner-Production
3. Configure Each Scheme:
Runner-Dev:
βββ Build Configuration: Debug
βββ Bundle Identifier: com.example.myapp.dev
βββ Display Name: MyApp Dev
βββ App Icon: AppIcon-Dev
Runner-Staging:
βββ Build Configuration: Release
βββ Bundle Identifier: com.example.myapp.staging
βββ Display Name: MyApp Staging
βββ App Icon: AppIcon-Staging
Runner-Production:
βββ Build Configuration: Release
βββ Bundle Identifier: com.example.myapp
βββ Display Name: MyApp
βββ App Icon: AppIcon
4. Add Environment Variables:
- Select scheme β Edit Scheme β Run β Arguments
- Add Environment Variables:
ENVIRONMENT = development (or staging/production)
π Pro Tip: Use Xcode's "User-Defined Settings" to manage configuration values that change between environments.
π Build Commands That Actually Work
# π΄ Development Build
flutter build apk --flavor development -t lib/main_development.dart
# π‘ Staging Build
flutter build apk --flavor staging -t lib/main_staging.dart
# π’ Production Build
flutter build apk --flavor production -t lib/main_production.dart
# π iOS Builds
flutter build ios --flavor production -t lib/main_production.dart
# π¦ App Bundle for Play Store
flutter build appbundle --flavor production -t lib/main_production.dart
π Bonus Script: Create a build script to simplify commands:
#!/bin/bash
# build.sh - Smart build script
ENVIRONMENT=$1
TARGET="lib/main_${ENVIRONMENT}.dart"
if [ -z "$ENVIRONMENT" ]; then
echo "Usage: ./build.sh [development|staging|production]"
exit 1
fi
echo "ποΈ Building for $ENVIRONMENT environment..."
# Clean previous builds
flutter clean
flutter pub get
# Build APK
flutter build apk --flavor $ENVIRONMENT -t $TARGET
# Build App Bundle for production
if [ "$ENVIRONMENT" = "production" ]; then
flutter build appbundle --flavor $ENVIRONMENT -t $TARGET
fi
echo "β Build complete!"
π€ CI/CD: Where the Magic Really Happens
π‘ What You'll Learn: How to set up a complete CI/CD pipeline with GitHub Actions that automatically tests, builds, and deploys your Flutter app across multiple environments.
After trying several platforms, we settled on GitHub Actions for its flexibility and integration with our existing workflow. Here's our battle-tested pipeline:
# .github/workflows/flutter_ci_cd.yml
# π€ Complete Flutter CI/CD Pipeline
name: Flutter CI/CD Pipeline
on:
push:
branches: [main, develop, 'release/**']
pull_request:
branches: [main, develop]
env:
FLUTTER_VERSION: '3.32.0' # π Pin Flutter version for consistency
jobs:
# π§ͺ Quality Gates - The Foundation
code_quality:
name: π§ͺ Code Quality & Testing
runs-on: ubuntu-latest
steps:
- name: π₯ Checkout Repository
uses: actions/checkout@v4
- name: π― Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true # π Speed up builds with caching
- name: π¦ Install Dependencies
run: flutter pub get
- name: π¨ Verify Formatting
run: dart format --output=none --set-exit-if-changed .
- name: π Analyze Code
run: flutter analyze
- name: π§ͺ Run Unit Tests
run: flutter test --coverage
- name: π Upload Coverage
uses: codecov/codecov-action@v3
with:
file: coverage/lcov.info
fail_ci_if_error: true
# ποΈ Build Strategy: Matrix for Multiple Environments
build_android:
name: ποΈ Build Android (${{ matrix.flavor }})
needs: code_quality # β Don't build if tests fail
runs-on: ubuntu-latest
strategy:
matrix:
flavor: [development, staging, production]
steps:
- name: π₯ Checkout Repository
uses: actions/checkout@v4
- name: π― Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: β Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: π¦ Install Dependencies
run: flutter pub get
# π Configure Android Signing (Production Only)
- name: π Configure Android Signing
if: matrix.flavor == 'production'
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/keystore.jks
echo "storePassword=${{ secrets.KEYSTORE_PASSWORD }}" >> android/key.properties
echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
echo "storeFile=keystore.jks" >> android/key.properties
# π± Build APK
- name: π± Build APK
run: |
flutter build apk \
--flavor ${{ matrix.flavor }} \
--target lib/main_${{ matrix.flavor }}.dart \
--dart-define=ENVIRONMENT=${{ matrix.flavor }} \
${{ matrix.flavor == 'production' && '--obfuscate --split-debug-info=build/debug-info' || '' }}
# π¦ Build App Bundle (Production Only)
- name: π¦ Build App Bundle (Production Only)
if: matrix.flavor == 'production'
run: |
flutter build appbundle \
--flavor ${{ matrix.flavor }} \
--target lib/main_${{ matrix.flavor }}.dart \
--dart-define=ENVIRONMENT=${{ matrix.flavor }} \
--obfuscate \
--split-debug-info=build/debug-info
# π€ Upload Build Artifacts
- name: π€ Upload APK Artifact
uses: actions/upload-artifact@v3
with:
name: apk-${{ matrix.flavor }}
path: build/app/outputs/flutter-apk/*.apk
retention-days: 30
- name: π€ Upload App Bundle (Production)
if: matrix.flavor == 'production'
uses: actions/upload-artifact@v3
with:
name: aab-production
path: build/app/outputs/bundle/**/*.aab
retention-days: 90
# π Deployment Strategy
deploy_to_firebase:
name: π Deploy to Firebase App Distribution
needs: build_android
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release/')
steps:
- name: π₯ Download Staging APK
uses: actions/download-artifact@v3
with:
name: apk-staging
- name: π Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
groups: internal-testers,qa-team # π₯ Distribution groups
file: app-staging-release.apk
releaseNotes: |
π New staging build from commit ${{ github.sha }}
π Details:
β’ Branch: ${{ github.ref_name }}
β’ Triggered by: ${{ github.actor }}
β’ Commit: ${{ github.event.head_commit.message }}
π§ͺ Ready for testing!
# πͺ Deploy to Play Store
deploy_to_play_store:
name: πͺ Deploy to Google Play (Internal Testing)
needs: build_android
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: π₯ Download Production App Bundle
uses: actions/download-artifact@v3
with:
name: aab-production
- name: πͺ Deploy to Play Store
uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.example.myapp
releaseFiles: '**/*.aab'
track: internal # π§ͺ Start with internal testing
status: completed
releaseNotes: |
π New version deployed automatically!
This release includes bug fixes and performance improvements.
π Pipeline Breakdown for Beginners:
| Stage | Purpose | When It Runs |
|---------------------|------------------------------------------|---------------------|
| π§ͺ Code Quality | Tests, linting, formatting | Every push/PR |
| ποΈ Build | Creates APKs/AABs for all environments | After tests pass |
| π Firebase Deploy | Distributes staging builds | On develop branch |
| πͺ Play Store Deploy | Uploads to Google Play | On main branch |
π Bonus Features:
- Caching: Speeds up builds by reusing dependencies
- Matrix Builds: Builds all environments in parallel
- Conditional Logic: Different steps for different environments
- Artifact Management: Stores builds with proper retention
π Secrets Management: Keep It Safe
Set up these secrets in your GitHub repository (Settings β Secrets and variables β Actions):
Required Secrets:
βββ π ANDROID_KEYSTORE_BASE64 # Your keystore file (base64 encoded)
βββ π KEYSTORE_PASSWORD # Password for keystore
βββ π KEY_PASSWORD # Password for signing key
βββ π KEY_ALIAS # Alias of your signing key
βββ π₯ FIREBASE_APP_ID # Firebase App Distribution ID
βββ π FIREBASE_SERVICE_ACCOUNT_JSON # Firebase service account
βββ πͺ GOOGLE_PLAY_SERVICE_ACCOUNT_JSON # Google Play Console service account
π How to Generate Secrets:
# π Convert keystore to base64
base64 -i android/app/keystore.jks | pbcopy
# π₯ Firebase service account
# 1. Go to Firebase Console β Project Settings β Service Accounts
# 2. Generate new private key
# 3. Copy entire JSON content
# πͺ Google Play service account
# 1. Go to Google Play Console β Setup β API access
# 2. Create service account
# 3. Download JSON key
π Advanced Patterns That Actually Work
π‘ What You'll Learn: Professional-grade patterns for feature flags, environment detection, and advanced deployment strategies used by major tech companies.
π― 1. Environment-Aware Feature Flags
Instead of hardcoding feature availability, we use a smart system:
// services/feature_flag_service.dart
// π― Smart feature flag system
class FeatureFlagService {
// π Main feature flag checker
static bool isFeatureEnabled(String featureKey) {
// π΄ In development, enable all features by default
if (AppConfig.instance.environment == 'development') {
return _getDevelopmentFeatureFlag(featureKey);
}
// π‘ In staging, use local configuration
if (AppConfig.instance.environment == 'staging') {
return _getStagingFeatureFlag(featureKey);
}
// π’ In production, use remote config
return _getProductionFeatureFlag(featureKey);
}
// π΄ Development feature flags - Liberal for testing
static bool _getDevelopmentFeatureFlag(String featureKey) {
const devFlags = {
'new_checkout_flow': true, // β Always test new features
'beta_dashboard': true, // β Show all beta features
'experimental_api': true, // β Enable experimental APIs
'debug_menu': true, // β Show debug options
'mock_data': true, // β Use mock data
};
return devFlags[featureKey] ?? true; // π Default to enabled
}
// π‘ Staging feature flags - Production-like but with beta features
static bool _getStagingFeatureFlag(String featureKey) {
const stagingFlags = {
'new_checkout_flow': true, // β Test before production
'beta_dashboard': true, // β QA testing
'experimental_api': false, // β Too risky for staging
'debug_menu': true, // β Debugging for QA
'mock_data': false, // β Use real staging data
};
return stagingFlags[featureKey] ?? false; // π Default to disabled
}
// π’ Production feature flags - Remote controlled
static bool _getProductionFeatureFlag(String featureKey) {
try {
// π Use Firebase Remote Config or similar
return FirebaseRemoteConfig.instance.getBool(featureKey);
} catch (e) {
// π‘οΈ Fail safely - return conservative defaults
const safeDefaults = {
'new_checkout_flow': false, // β Safe default
'beta_dashboard': false, // β Safe default
'experimental_api': false, // β Never enable by default
'debug_menu': false, // β Never show in production
'mock_data': false, // β Always use real data
};
return safeDefaults[featureKey] ?? false; // π‘οΈ Ultra-conservative default
}
}
// π Feature flag analytics
static void trackFeatureUsage(String featureKey, bool wasEnabled) {
if (AppConfig.instance.environment != 'production') return;
// Track which features are actually being used
Analytics.track('feature_flag_accessed', {
'feature': featureKey,
'enabled': wasEnabled,
'environment': AppConfig.instance.environment,
});
}
}
π― Usage Example:
// widgets/new_checkout_button.dart
class NewCheckoutButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ποΈ Feature flag check
final useNewCheckout = FeatureFlagService.isFeatureEnabled('new_checkout_flow');
if (useNewCheckout) {
return NewCheckoutWidget(); // π New implementation
}
return LegacyCheckoutWidget(); // ποΈ Fallback to old version
}
}
π Pro Tip: Always have a fallback. Feature flags should degrade gracefully, not crash your app.
π―2. Smart Environment Detection
Sometimes you need to make runtime decisions based on the build environment:
// utils/environment_detector.dart
// π΅οΈ Smart environment detection utility
class EnvironmentDetector {
// π― Main detection method
static String detectEnvironment() {
// π Method 1: Check if we have a configured environment
try {
return AppConfig.instance.environment;
} catch (e) {
// AppConfig not initialized yet
}
// π Method 2: Check build mode
if (kDebugMode) {
return 'development';
}
// π Method 3: Check app bundle ID (Android/iOS)
return _detectFromBundleId();
}
// π± Bundle ID detection
static String _detectFromBundleId() {
final packageInfo = PackageInfo.fromPlatform();
return packageInfo.then((info) {
final bundleId = info.packageName;
if (bundleId.endsWith('.dev')) {
return 'development';
} else if (bundleId.endsWith('.staging')) {
return 'staging';
} else {
return 'production';
}
}).catchError((_) => 'unknown');
}
// π§ͺ Quick environment checks
static bool get isDevelopment => detectEnvironment() == 'development';
static bool get isStaging => detectEnvironment() == 'staging';
static bool get isProduction => detectEnvironment() == 'production';
// π Environment-specific behavior
static T environmentSwitch<T>({
required T development,
required T staging,
required T production,
}) {
switch (detectEnvironment()) {
case 'development':
return development;
case 'staging':
return staging;
case 'production':
default:
return production;
}
}
}
π― Usage Examples:
// π¨ Environment-specific styling
final primaryColor = EnvironmentDetector.environmentSwitch(
development: Colors.red, // π΄ Red for dev
staging: Colors.orange, // π Orange for staging
production: Colors.blue, // π΅ Blue for production
);
// π Environment-specific URLs
final wsUrl = EnvironmentDetector.environmentSwitch(
development: 'ws://localhost:8080',
staging: 'wss://staging-ws.myapp.com',
production: 'wss://ws.myapp.com',
);
// π Environment-specific analytics
if (EnvironmentDetector.isProduction) {
Analytics.initialize(); // Only initialize in production
}
π― 3. Advanced Build Automation
Here's our production-grade build script that handles everything:
#!/bin/bash
# scripts/build_and_deploy.sh
# π Advanced build automation script
set -e # Exit on any error
# π¨ Colors for pretty output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# π Configuration
ENVIRONMENT=${1:-development}
BUILD_TYPE=${2:-apk}
DEPLOY=${3:-false}
# π― Validate inputs
validate_inputs() {
if [[ ! "$ENVIRONMENT" =~ ^(development|staging|production)$ ]]; then
echo -e "${RED}β Invalid environment: $ENVIRONMENT${NC}"
echo "Usage: ./build_and_deploy.sh [development|staging|production] [apk|aab] [true|false]"
exit 1
fi
if [[ ! "$BUILD_TYPE" =~ ^(apk|aab|ios)$ ]]; then
echo -e "${RED}β Invalid build type: $BUILD_TYPE${NC}"
exit 1
fi
}
# π§Ή Clean and prepare
clean_and_prepare() {
echo -e "${BLUE}π§Ή Cleaning previous builds...${NC}"
flutter clean
flutter pub get
# π Run code quality checks
echo -e "${BLUE}π Running code quality checks...${NC}"
dart format --set-exit-if-changed .
flutter analyze
# π§ͺ Run tests
echo -e "${BLUE}π§ͺ Running tests...${NC}"
flutter test
}
# ποΈ Build application
build_app() {
local target="lib/main_${ENVIRONMENT}.dart"
local flavor_arg=""
if [[ "$BUILD_TYPE" != "ios" ]]; then
flavor_arg="--flavor $ENVIRONMENT"
fi
echo -e "${BLUE}ποΈ Building $BUILD_TYPE for $ENVIRONMENT...${NC}"
case $BUILD_TYPE in
"apk")
flutter build apk $flavor_arg -t $target \
--dart-define=ENVIRONMENT=$ENVIRONMENT \
$([ "$ENVIRONMENT" = "production" ] && echo "--obfuscate --split-debug-info=build/debug-info")
;;
"aab")
flutter build appbundle $flavor_arg -t $target \
--dart-define=ENVIRONMENT=$ENVIRONMENT \
$([ "$ENVIRONMENT" = "production" ] && echo "--obfuscate --split-debug-info=build/debug-info")
;;
"ios")
flutter build ios -t $target \
--dart-define=ENVIRONMENT=$ENVIRONMENT \
$([ "$ENVIRONMENT" = "production" ] && echo "--obfuscate --split-debug-info=build/debug-info")
;;
esac
}
# π€ Deploy application
deploy_app() {
if [[ "$DEPLOY" != "true" ]]; then
echo -e "${YELLOW}βοΈ Skipping deployment (DEPLOY=$DEPLOY)${NC}"
return
fi
echo -e "${BLUE}π€ Deploying $ENVIRONMENT build...${NC}"
case $ENVIRONMENT in
"development")
echo -e "${YELLOW}π± Development builds are not deployed automatically${NC}"
;;
"staging")
deploy_to_firebase
;;
"production")
deploy_to_stores
;;
esac
}
# π₯ Deploy to Firebase App Distribution
deploy_to_firebase() {
if ! command -v firebase &> /dev/null; then
echo -e "${RED}β Firebase CLI not found. Install with: npm install -g firebase-tools${NC}"
exit 1
fi
echo -e "${BLUE}π₯ Deploying to Firebase App Distribution...${NC}"
firebase appdistribution:distribute \
build/app/outputs/flutter-apk/app-staging-release.apk \
--app "$FIREBASE_APP_ID" \
--groups "internal-testers,qa-team" \
--release-notes "Automated staging build from $(git rev-parse --short HEAD)"
}
# πͺ Deploy to app stores
deploy_to_stores() {
echo -e "${BLUE}πͺ Deploying to app stores...${NC}"
# This would integrate with your store deployment tools
# For example, using Fastlane or direct API calls
echo -e "${YELLOW}π§ Store deployment requires manual approval${NC}"
echo -e "${GREEN}β Production build ready for store submission${NC}"
}
# π Generate build report
generate_report() {
local build_size=""
local build_path=""
case $BUILD_TYPE in
"apk")
build_path="build/app/outputs/flutter-apk/app-${ENVIRONMENT}-release.apk"
;;
"aab")
build_path="build/app/outputs/bundle/${ENVIRONMENT}Release/app-${ENVIRONMENT}-release.aab"
;;
esac
if [[ -f "$build_path" ]]; then
build_size=$(du -h "$build_path" | cut -f1)
echo -e "${GREEN}π Build Report:${NC}"
echo -e " π± Environment: $ENVIRONMENT"
echo -e " π¦ Type: $BUILD_TYPE"
echo -e " π Size: $build_size"
echo -e " π Location: $build_path"
echo -e " π Built: $(date)"
fi
}
# π Main execution
main() {
echo -e "${GREEN}π Starting Flutter build process...${NC}"
validate_inputs
clean_and_prepare
build_app
deploy_app
generate_report
echo -e "${GREEN}β Build process completed successfully!${NC}"
}
# Execute main function
main "$@"
π― Usage Examples:
# π΄ Development build