DEV Community

Terence Faid JABO
Terence Faid JABO

Posted on • Edited on

Adding Flavors to Your Flutter App: From One Codebase to Multiple Experiences

You're building a Flutter app. Your client wants three versions: development for testing, staging for final checks, and production for real users. Each needs different settings—different API URLs, app names, even colors.

Your first thought might be: "I'll create three separate projects."

Don't. There's a better way.

What Are Flutter Flavors?

Flutter flavors let you build multiple versions of your app from one codebase. Think of it like a restaurant kitchen making different dishes from the same ingredients.

One codebase → Multiple app versions

Each flavor can have its own:

  • App name and icon
  • API endpoints
  • Colors and branding
  • Feature toggles
  • Configuration settings

The Problem Flavors Solve

Without flavors, developers face these headaches:

Environment Mix-ups: Accidentally testing against production data, breaking real user experiences.

Manual Switching: Constantly changing API URLs and settings in code, leading to errors and messy commits.

Code Duplication: Maintaining separate codebases for different clients or environments.

Deployment Confusion: Forgetting which configuration is active before releasing.

The Solution in Action

With flavors, switching environments becomes effortless:

# Launch development version
flutter run --flavor dev

# staging version  
flutter run --flavor staging

# production version
flutter run --flavor prod
Enter fullscreen mode Exit fullscreen mode

Each command launches a completely different app configuration. Same code, different behavior.

Real Benefits You'll See Immediately

Faster Development: No more manually changing configurations. Switch environments with one command.

Fewer Bugs: Each environment is isolated. No more production accidents during development.

Easier Testing: QA team gets their own staging environment. Developers can experiment freely in dev.

Client Customization: Build white-label apps for multiple clients without duplicating code.

Setting Up Flavors: Choose Your Approach

Option 1: Manual Setup (Full Control)

Perfect when you need complete customization or want to understand the underlying mechanics.

Step 1: Configure Android Flavors

Create android/app/build.gradle:

android {
    flavorDimensions "default"

    productFlavors {
        dev {
            dimension "default"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
            resValue "string", "app_name", "MyApp Dev"
        }

        staging {
            dimension "default"
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
            resValue "string", "app_name", "MyApp Staging"
        }

        prod {
            dimension "default"
            resValue "string", "app_name", "MyApp"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure iOS Flavors

Create build configurations in Xcode:

  1. Open ios/Runner.xcworkspace
  2. Select Runner project → Info tab
  3. Duplicate existing configurations
  4. Create: Debug-dev, Debug-staging, Debug-prod, Release-dev, Release-staging, Release-prod

Step 3: Create Dart Configuration Files

Create lib/config/app_config.dart:

class AppConfig {
  static const String appName = String.fromEnvironment('APP_NAME');
  static const String apiUrl = String.fromEnvironment('API_URL');
  static const String flavor = String.fromEnvironment('FLAVOR');

  static bool get isDevelopment => flavor == 'dev';
  static bool get isStaging => flavor == 'staging';
  static bool get isProduction => flavor == 'prod';
}
Enter fullscreen mode Exit fullscreen mode

Create flavor-specific entry points:

lib/main_dev.dart:

import 'main.dart' as app;

void main() {
  app.main();
}
Enter fullscreen mode Exit fullscreen mode

Option 2: Very Good CLI (Quick Setup)

Great for rapid prototyping and standard configurations.

# Install the CLI
dart pub global activate very_good_cli

# Create project with flavors pre-configured
very_good create flutter_app my_app
Enter fullscreen mode Exit fullscreen mode

You get development, staging, and production flavors ready to use with proper native configurations.

Option 3: Flavorizr Package (Automated Setup)

Ideal for existing projects that need flavors added quickly.

# Add to pubspec.yaml
dev_dependencies:
  flavorizr: ^2.2.1

# Create flavorizr.yaml configuration
flutter packages pub run flavorizr
Enter fullscreen mode Exit fullscreen mode

Adding Custom Flavors

Need more than dev/staging/prod? Here's how to add custom flavors:

Adding a "Demo" Flavor

Update Android Configuration

Add to android/app/build.gradle:

productFlavors {
    // existing flavors...

    demo {
        dimension "default"
        applicationIdSuffix ".demo"
        versionNameSuffix "-demo"
        resValue "string", "app_name", "MyApp Demo"
    }
}
Enter fullscreen mode Exit fullscreen mode

Update iOS Configuration

  1. In Xcode, duplicate existing configurations
  2. Create Debug-demo and Release-demo
  3. Update build settings for demo-specific values

Create Dart Entry Point

Create lib/main_demo.dart:

import 'package:flutter/material.dart';
import 'main.dart' as app;

void main() {
  const String.fromEnvironment('FLAVOR', defaultValue: 'demo');
  app.main();
}
Enter fullscreen mode Exit fullscreen mode

Update Launch Configuration

Create .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Demo",
            "request": "launch",
            "type": "dart",
            "program": "lib/main_demo.dart",
            "args": ["--flavor", "demo", "--dart-define", "FLAVOR=demo"]
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Advanced Flavor Configurations

Environment-Specific Features

class FeatureFlags {
  static bool get debugMode => AppConfig.isDevelopment;
  static bool get analyticsEnabled => !AppConfig.isDevelopment;
  static bool get crashReporting => AppConfig.isProduction;
}
Enter fullscreen mode Exit fullscreen mode

Different Themes Per Flavor

class FlavorTheme {
  static ThemeData get theme {
    switch (AppConfig.flavor) {
      case 'dev':
        return ThemeData(primarySwatch: Colors.red);
      case 'staging':
        return ThemeData(primarySwatch: Colors.orange);
      case 'demo':
        return ThemeData(primarySwatch: Colors.purple);
      default:
        return ThemeData(primarySwatch: Colors.blue);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Client-Specific Configurations

class ClientConfig {
  static const String clientName = String.fromEnvironment('CLIENT_NAME');
  static const String brandColor = String.fromEnvironment('BRAND_COLOR');

  static Color get primaryColor {
    switch (clientName) {
      case 'client_a':
        return Colors.blue;
      case 'client_b':
        return Colors.green;
      default:
        return Colors.grey;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Running Your Flavored App

Command Line

# Development
flutter run --flavor dev --dart-define=FLAVOR=dev

# Staging
flutter run --flavor staging --dart-define=FLAVOR=staging

# Custom demo flavor
flutter run --flavor demo --dart-define=FLAVOR=demo
Enter fullscreen mode Exit fullscreen mode

Building for Release

# Build production APK
flutter build apk --flavor prod --dart-define=FLAVOR=prod

# Build staging iOS
flutter build ios --flavor staging --dart-define=FLAVOR=staging
Enter fullscreen mode Exit fullscreen mode

Best Practices

Keep Configurations Organized

lib/
├── config/
│   ├── app_config.dart
│   ├── dev_config.dart
│   ├── staging_config.dart
│   └── prod_config.dart
├── main.dart
├── main_dev.dart
├── main_staging.dart
└── main_prod.dart
Enter fullscreen mode Exit fullscreen mode

Use Build-Time Constants

class AppConstants {
  static const String apiUrl = String.fromEnvironment(
    'API_URL',
    defaultValue: 'https://api.example.com',
  );

  static const bool enableLogging = bool.fromEnvironment(
    'ENABLE_LOGGING',
    defaultValue: false,
  );
}
Enter fullscreen mode Exit fullscreen mode

Version Control Considerations

Never commit sensitive data like API keys directly. Use environment variables:

class Secrets {
  static const String apiKey = String.fromEnvironment('API_KEY');
  static const String databaseUrl = String.fromEnvironment('DATABASE_URL');
}
Enter fullscreen mode Exit fullscreen mode

Your Development Workflow Transformed

Before flavors:

  1. Change API URL in code
  2. Update app name manually
  3. Switch configuration files
  4. Hope you didn't miss anything
  5. Commit changes (cluttering git history)

With flavors:

  1. Run flutter run --flavor dev
  2. Done.

Troubleshooting Common Issues

iOS Build Errors

Make sure all configurations exist in Xcode and schemes are properly configured.

Android Namespace Conflicts

Ensure each flavor has a unique applicationIdSuffix.

Missing Dependencies

Some flavors might need specific dependencies. Use flavor-specific dependency injection.

Start Using Flavors Today

If you're managing multiple environments or building for different clients, flavors aren't optional—they're essential.

Set them up once, save time forever.

Your codebase stays clean. Your deployments become predictable. Your team stays focused on building features, not managing configurations.

Choose the setup method that works best for your project and team—whether that's manual configuration for maximum control, Very Good CLI for quick setup, or Flavorizr for existing projects.

Top comments (0)