πŸ—οΈ Real-World Build Variants, Environments & CI/CD for Flutter Projects (Part 1)

πŸ—οΈ Real-World Build Variants, Environments & CI/CD for Flutter Projects (Part 1)

FlutterPulse

This 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:

  1. Build the staging version manually
  2. Test it on a few devices
  3. Build the production version (fingers crossed we didn't mess up the config)
  4. Upload to app stores
  5. 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 follow
  • static late: This creates a single instance shared across the entire app
  • get: 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 method
  • const 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 versions
  • versionNameSuffix: Helps identify which version you're looking at
  • resValue: Creates Android resources (like strings) at build time
  • signingConfig: 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:

  1. Open Xcode β†’ Open ios/Runner.xcworkspace
  2. 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

Report Page