Lesson 38 of 77 intermediate

Flavors, Signing, Release Builds & Store Workflows

Managing dev/staging/prod environments, Android signing, and app store submission pipelines

Open interactive version (quiz + challenge)

Real-world analogy

Build flavors are like a restaurant having separate menus for staff meals, soft openings, and the public — same kitchen, different ingredients and branding. Signing is the restaurant's health certificate — the store will not let you serve food without it.

What is it?

Build flavors enable managing multiple app environments (dev/staging/prod) from one codebase. Android signing with keystores and iOS provisioning profiles are mandatory for release builds. Understanding the full store submission pipeline is a key senior Flutter engineer competency.

Real-world relevance

On a fintech claims app, the team ran BankID auth testing against the production BankID endpoint from the dev flavor because the API_BASE_URL was hardcoded. After setting up flavors with dart-define, dev flavor pointed to the sandbox, preventing real BankID calls in development.

Key points

Code example

// === android/app/build.gradle — flavor configuration ===
android {
    flavorDimensions "environment"

    productFlavors {
        development {
            dimension "environment"
            applicationId "com.example.app.dev"
            resValue "string", "app_name", "MyApp Dev"
            versionNameSuffix "-dev"
        }
        staging {
            dimension "environment"
            applicationId "com.example.app.staging"
            resValue "string", "app_name", "MyApp Staging"
        }
        production {
            dimension "environment"
            applicationId "com.example.app"
            resValue "string", "app_name", "MyApp"
        }
    }

    signingConfigs {
        release {
            def keystoreProperties = new Properties()
            def keystorePropertiesFile = rootProject.file('app/key.properties')
            if (keystorePropertiesFile.exists()) {
                keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
            }
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
        }
    }
}

// === lib/config/flavor_config.dart ===
enum Flavor { development, staging, production }

class FlavorConfig {
  final Flavor flavor;
  final String apiBaseUrl;
  final bool enableCrashlytics;

  static late FlavorConfig _instance;
  static FlavorConfig get instance => _instance;

  FlavorConfig._({
    required this.flavor,
    required this.apiBaseUrl,
    required this.enableCrashlytics,
  });

  static void initialize({
    required Flavor flavor,
    required String apiBaseUrl,
    required bool enableCrashlytics,
  }) {
    _instance = FlavorConfig._(
      flavor: flavor,
      apiBaseUrl: apiBaseUrl,
      enableCrashlytics: enableCrashlytics,
    );
  }
}

// === main_production.dart ===
void main() {
  FlavorConfig.initialize(
    flavor: Flavor.production,
    apiBaseUrl: 'https://api.example.com',
    enableCrashlytics: true,
  );
  runApp(const MyApp());
}

// === Build commands ===
// flutter run --flavor development -t lib/main_development.dart
// flutter build appbundle --release --flavor production -t lib/main_production.dart

Line-by-line walkthrough

  1. 1. productFlavors { development { applicationId 'com.example.app.dev' } } — different bundle ID per flavor lets dev and prod be installed side by side on the same device
  2. 2. resValue 'string', 'app_name', 'MyApp Dev' — overrides the app name shown on the home screen per flavor
  3. 3. signingConfigs.release reads from key.properties — the file is gitignored; CI injects it from secrets at build time
  4. 4. minifyEnabled true — enables R8 code shrinking and obfuscation in release builds
  5. 5. class FlavorConfig — singleton pattern with static late _instance; initialised in main_production.dart before runApp
  6. 6. main_production.dart sets apiBaseUrl to the real API — each flavor has its own main file as the entry point
  7. 7. flutter build appbundle --release --flavor production -t lib/main_production.dart — specifies both the flavor and entry point

Spot the bug

// android/app/build.gradle
signingConfigs {
  release {
    keyAlias 'my-key'
    keyPassword 'supersecret123'
    storeFile file('keystore.jks')
    storePassword 'keystorepass456'
  }
}
Need a hint?
This will build successfully but creates a serious security problem.
Show answer
Bug: The signing credentials (keyAlias, keyPassword, storeFile, storePassword) are hardcoded directly in build.gradle. If this file is committed to git (which it usually is), the keystore password is exposed to everyone with repo access — and permanently in git history even if later removed. Fix: move credentials to android/key.properties (add to .gitignore), load them with Properties() at build time, and store the actual values as CI secrets injected at build time.

Explain like I'm 5

Imagine your app is a pizza. Flavors let you make a 'development pizza' (cheap ingredients, messy, for the chefs to taste), a 'staging pizza' (almost the real thing, for trusted friends to review), and a 'production pizza' (perfect, for paying customers). Signing is the health certificate that says the pizza was made in a licensed kitchen — the app store refuses unsigned pizzas.

Fun fact

The Android App Bundle (AAB) format, required for Play Store since August 2021, allows Google Play to generate optimised APKs per device configuration. This typically reduces app download size by 15-20% compared to a universal APK.

Hands-on challenge

Describe (in interview-ready language) how you would set up three flavors for a fintech app: dev (sandbox API, debug signing), staging (staging API, debug signing), production (production API, release signing). Include how you would prevent the keystore from being committed to git.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery