Flutter Flavor Orchestrator

Buy me a coffee on Ko-fi - If this package helps you, consider supporting my work!

CI pub package pub points popularity likes License: MIT Ko-fi

Build-time orchestration for Flutter flavors across Android and iOS. Configure environment-specific app identity, native metadata, provisioning files, and resource mappings from a single YAML source.

What's New (v0.5.0)

  • Automatic backup before apply — Every non-dry-run apply now snapshots all destination files into .ffo/backups/ before touching them. Each backup stores file content, pre-apply checksums, and (after a successful apply) post-apply checksums so that manual edits can be detected.
  • rollback command — Restore project files to their pre-apply state: flutter_flavor_orchestrator rollback --latest
  • rollback --id <id> — Restore from a specific backup by its identifier.
  • rollback --force — Override checksum conflicts when files were edited after the last apply.
  • FlavorOrchestrator.rollbackLatest() / rollbackById() — New public API methods for programmatic rollback.

Previous: What's New (v0.4.0)

  • plan command — Preview operations without mutating files: flutter_flavor_orchestrator plan --flavor dev
  • --output json — Machine-readable plan output: plan --flavor dev --output json
  • FlavorOrchestrator.planFlavor() — New public method returning an ExecutionPlan without executing it

Previous: What's New (v0.3.0)

  • Typed Operation ModelsOperationKind, PlannedOperation, ExecutionPlan added as public API; each operation is now a first-class immutable value with toJson() support
  • Shared Planning FoundationFlavorOrchestrator builds an ExecutionPlan before executing any apply; now exposed publicly via planFlavor() for the plan command
  • AssetProcessor.planFileMappings() — generate file-mapping operations without touching the file system (preview without side-effects)

Previous: What's New (v0.2.0)

  • Dry-run Apply Mode (--dry-run) - Execute full apply processing without changing files
  • Destination Presence Validation - Dry-run validates destination files/directories exist for every write/copy path
  • Safer Preflight for CI/CD - Validate flavor application end-to-end before running a real apply

✨ Features

  • Flavor management - Configure dev, staging, production, and custom environments
  • Cross-platform native updates - Apply changes to Android and iOS project files
  • Native file processors - Update AndroidManifest, Gradle/Gradle KTS, Info.plist, and Xcode project settings
  • Format preservation - Keep original AndroidManifest indentation and structure
  • Provisioning support - Manage google-services.json and GoogleService-Info.plist
  • File mappings - Copy flavor-specific files and recursive directories
  • Atomic directory replacement - Backup/restore-safe replacement of destination directories
  • Persistent backup & rollback — Automatic pre-apply snapshots with checksum validation and rollback CLI command
  • YAML-driven configuration - Single declarative config for all flavors
  • Typed execution plan - ExecutionPlan, PlannedOperation, OperationKind models with toJson()
  • CLI workflow - apply, plan, rollback, list, info, and validate commands
  • plan command — Preview operations without file mutations; text and JSON output
  • Validation and error handling - Pre-checks for config, files, and required fields
  • Documentation and examples - Full example project and practical guides

📋 Table of Contents

🚀 Installation

Add flutter_flavor_orchestrator to your pubspec.yaml dev dependencies:

dev_dependencies:
  flutter_flavor_orchestrator: ^0.5.0

Then run:

flutter pub get

Activate the CLI tool globally (optional):

dart pub global activate flutter_flavor_orchestrator

⚡ Quick Start

1. Create Configuration File

Create a flavor_config.yaml file in your project root:

dev:
  bundle_id: com.example.myapp.dev
  app_name: MyApp Dev
  metadata:
    API_URL: https://dev-api.example.com
  provisioning:
    android_google_services: configs/dev/google-services.json
    ios_google_service: configs/dev/GoogleService-Info.plist

production:
  bundle_id: com.example.myapp
  app_name: MyApp
  metadata:
    API_URL: https://api.example.com
  provisioning:
    android_google_services: configs/production/google-services.json
    ios_google_service: configs/production/GoogleService-Info.plist

2. Apply a Flavor

# From your project root
flutter pub run flutter_flavor_orchestrator apply --flavor dev

# Alternative (Dart-native invocation)
dart run flutter_flavor_orchestrator apply --flavor dev

3. Build Your App

flutter clean
flutter pub get
flutter build apk  # or flutter build ios

That's it! Your app is now configured for the dev flavor.

Pub.dev Workflow

Use this sequence when integrating the package into a real app:

# 1) Install deps
flutter pub get

# 2) Validate configuration before applying
flutter pub run flutter_flavor_orchestrator validate

# 3) Inspect available flavors
flutter pub run flutter_flavor_orchestrator list

# 4) Apply flavor
flutter pub run flutter_flavor_orchestrator apply --flavor dev --verbose

# 5) Build or run
flutter run

Recommended for CI/CD:

  • Run validate as an early pipeline step
  • Use explicit flavor names (dev, staging, production) in build jobs
  • Keep flavor-specific files under configs/, assets/, and resources/
  • Pass --config to load a YAML file from a secure external path (for example Jenkins workspace/secret mounts)

⚙️ Configuration

Configuration File Location

You can place your flavor configuration in either:

  1. Dedicated file: flavor_config.yaml in your project root (recommended)
  2. In pubspec.yaml: Under a flavor_config section
  3. External file path: Any YAML file passed at runtime with --config

External path examples:

flutter_flavor_orchestrator apply --flavor production --config /secure/jenkins/flavor_config.yaml
flutter_flavor_orchestrator validate --config ./ci/flavor_config.yaml

Configuration Options

Each flavor supports the following configuration options:

Option Type Required Description
bundle_id String Bundle identifier (iOS) / Package name (Android)
app_name String Display name of the application
icon_path String Path to app icon assets
metadata Map Custom key-value pairs to inject into manifests
assets List Flavor-specific asset paths
dependencies Map Flavor-specific dependency overrides
provisioning Object Provisioning file configuration
android_min_sdk_version Integer Android minimum SDK version
android_target_sdk_version Integer Android target SDK version
android_compile_sdk_version Integer Android compile SDK version
ios_min_version String iOS minimum deployment target
custom_gradle_config Map Custom Gradle configuration snippets
custom_info_plist_entries Map Custom Info.plist entries
file_mappings Map Flavor-specific file/folder copying (source→destination)
replace_destination_directories Boolean Replace existing directories completely (default: false)

Provisioning Configuration

provisioning:
  android_google_services: path/to/google-services.json
  ios_google_service: path/to/GoogleService-Info.plist
  additional_files:
    destination/path: source/path

File Mappings (New in v0.1.7)

Copy flavor-specific files and folders from source to destination paths. Supports both individual files and recursive directory copying:

dev:
  bundle_id: com.example.app.dev
  app_name: MyApp Dev
  
  # Enable complete directory replacement (optional, default: false)
  replace_destination_directories: true
  
  file_mappings:
    # Copy individual configuration files
    'lib/config/app_config.dart': 'configs/dev/app_config.dart'
    'lib/config/constants.dart': 'configs/shared/constants.dart'
    
    # Copy flavor-specific icons
    'assets/app_icon.svg': 'assets/icons/dev/app_icon.svg'
    
    # Recursively copy entire directories
    'android/app/src/main/res/drawable': 'assets/dev/android/drawables'
    'ios/Runner/Assets.xcassets': 'assets/dev/ios/assets'
    
    # Replace entire theme directory (useful with replace_destination_directories)
    'lib/theme': 'resources/dev/themes'

production:
  bundle_id: com.example.app
  app_name: MyApp
  replace_destination_directories: true
  file_mappings:
    'lib/config/app_config.dart': 'configs/production/app_config.dart'
    'lib/config/constants.dart': 'configs/shared/constants.dart'
    'assets/app_icon.svg': 'assets/icons/production/app_icon.svg'
    'lib/theme': 'resources/production/themes'

Features:

  • 📁 Copy individual files or entire directory trees recursively
  • 🔄 Automatically replaces existing files at destination
  • 📂 Creates destination directories if they don't exist
  • 🔍 Detailed logging for each file operation
  • ⚠️ Skips non-existent source paths with warnings
  • 🔙 Full backup and rollback support

Directory Replacement Mode:

When replace_destination_directories: true:

  1. 🔒 Safe Backup: Existing destination directory is temporarily renamed
  2. 📋 Copy New: Complete directory tree is copied from source
  3. Success: Backup directory is removed
  4. Failure: Original directory is automatically restored

This ensures atomic directory replacement - the destination is either completely replaced or left unchanged.

See example/assets/icons/README.md and example/resources/README.md for practical examples.

Complete Example

See example/flavor_config.yaml for a comprehensive configuration example.

🎮 CLI Usage

The package provides a powerful command-line interface:

Apply Command

Apply a flavor configuration to your project:

# Apply to both platforms
flutter_flavor_orchestrator apply --flavor dev

# Apply using an external YAML config file
flutter_flavor_orchestrator apply --flavor production --config /secure/jenkins/flavor_config.yaml

# Apply to Android only
flutter_flavor_orchestrator apply --flavor staging --platform android

# Dry-run apply (execute checks without changing files)
flutter_flavor_orchestrator apply --flavor dev --dry-run

# Apply to iOS only
flutter_flavor_orchestrator apply --flavor production --platform ios

# Enable verbose output
flutter_flavor_orchestrator apply --flavor dev --verbose

Output includes:

  • file_mappings count for the selected flavor
  • replace_destination_directories value
  • Mapping details (destination <- source) when --verbose is enabled
  • In --dry-run, all operations are validated and no files are modified

Plan Command

Preview the operations that would be performed for a flavor, without mutating any files:

# Preview both platforms (text output)
flutter_flavor_orchestrator plan --flavor dev

# Preview as machine-readable JSON
flutter_flavor_orchestrator plan --flavor dev --output json

# Preview Android-only plan
flutter_flavor_orchestrator plan --flavor staging --platform android

# Preview from an external YAML config
flutter_flavor_orchestrator plan --flavor production --config /secure/jenkins/flavor_config.yaml

Output includes a per-platform section listing each operation with its kind (copyFile, copyDirectory, writeFile, skip), description, and source/destination paths.

JSON output (--output json) returns the serialised ExecutionPlan with stable top-level keys:

  • flavor — flavor name
  • platforms — target platforms
  • total_operations, active_operations, skipped_operations — operation counts
  • operations — ordered list of operation objects

Rollback Command

Restore project files to their pre-apply state from a persistent backup created by the last apply run:

# Rollback to the most recent backup
flutter_flavor_orchestrator rollback --latest

# Rollback to a specific backup by ID (shown in the apply log)
flutter_flavor_orchestrator rollback --id 20260225_194640123_dev

# Force rollback even if files were manually edited after the last apply
flutter_flavor_orchestrator rollback --latest --force

Every non-dry-run apply automatically creates a snapshot in .ffo/backups/ before touching any files. The backup stores:

  • A copy of every destination file before the apply
  • SHA-256 checksums of the pre-apply content
  • SHA-256 checksums of the post-apply content (for detecting manual edits)

If files have been manually edited after the last apply, rollback will report a conflict and exit with code 1. Pass --force to override.

List Command

List all available flavors:

flutter_flavor_orchestrator list

# List flavors from external YAML
flutter_flavor_orchestrator list --config ./ci/flavor_config.yaml

Output includes, for each flavor:

  • file_mappings count
  • replace_destination_directories value

Info Command

Display detailed information about a specific flavor:

flutter_flavor_orchestrator info --flavor production

# Inspect a flavor from external YAML
flutter_flavor_orchestrator info --flavor production --config ./ci/flavor_config.yaml

Output includes:

  • Full file_mappings entries (destination <- source)
  • replace_destination_directories value
  • Explanation of when directory replacement applies and rollback behavior

Validate Command

Validate all flavor configurations:

flutter_flavor_orchestrator validate

# Validate external YAML
flutter_flavor_orchestrator validate --config ./ci/flavor_config.yaml

Output includes, for each flavor:

  • file_mappings count
  • replace_destination_directories value
  • A note when directory replacement is enabled for directory mappings

Help

Display help information:

flutter_flavor_orchestrator --help

🔨 What Gets Modified

Android

When you apply a flavor, the following Android files are automatically updated:

android/app/src/main/AndroidManifest.xml

  • ✏️ Package name (package attribute)
  • ✏️ Application label (android:label)
  • ✏️ Metadata entries (<meta-data> tags)
  • 🎨 Preserves original formatting - Maintains exact indentation, whitespace, and structure

android/app/build.gradle or android/app/build.gradle.kts

  • ✏️ Application ID (applicationId)
  • ✏️ SDK versions (minSdkVersion, targetSdkVersion, compileSdkVersion)
  • ✏️ Custom Gradle configuration
  • 🔄 Supports both Groovy (.gradle) and Kotlin (.gradle.kts) build scripts

android/app/google-services.json

  • 📋 Copied from configured path

iOS

ios/Runner/Info.plist

  • ✏️ Bundle display name (CFBundleDisplayName)
  • ✏️ Bundle identifier (CFBundleIdentifier)
  • ✏️ Minimum OS version (MinimumOSVersion)
  • ✏️ Custom plist entries

ios/Runner.xcodeproj/project.pbxproj

  • ✏️ Product bundle identifier (PRODUCT_BUNDLE_IDENTIFIER)
  • ✏️ Deployment target (IPHONEOS_DEPLOYMENT_TARGET)

ios/Runner/GoogleService-Info.plist

  • 📋 Copied from configured path

🏗️ Advanced Configuration

Custom Gradle Configuration

Inject custom Gradle snippets:

custom_gradle_config:
  defaultConfig: |
    buildConfigField "String", "API_URL", "\"https://api.example.com\""
    buildConfigField "boolean", "DEBUG_MODE", "false"
  buildTypes: |
    release {
        shrinkResources true
        minifyEnabled true
    }

Custom Info.plist Entries

Add custom iOS configuration:

custom_info_plist_entries:
  NSAppTransportSecurity:
    NSAllowsArbitraryLoads: true
  UIBackgroundModes:
    - fetch
    - remote-notification
  ITSAppUsesNonExemptEncryption: false

Metadata Injection

Add custom metadata to both platforms:

metadata:
  API_URL: https://api.example.com
  API_KEY: your_api_key
  FEATURE_FLAG_X: true
  MAX_RETRIES: 3

Android: Added as <meta-data> tags in AndroidManifest.xml

iOS: Added as custom entries in Info.plist

🏛️ Architecture

The package follows Clean Architecture principles:

lib/
├── src/
│   ├── models/              # Data models
│   │   ├── execution_plan.dart      # Ordered plan of PlannedOperations (v0.3.0)
│   │   ├── flavor_config.dart
│   │   ├── operation_kind.dart      # OperationKind enum (v0.3.0)
│   │   ├── planned_operation.dart   # Single step descriptor (v0.3.0)
│   │   └── provisioning_config.dart
│   ├── processors/          # Platform processors
│   │   ├── android_processor.dart
│   │   ├── asset_processor.dart     # planFileMappings() added (v0.3.0)
│   │   └── ios_processor.dart
│   ├── utils/              # Utilities
│   │   ├── file_manager.dart
│   │   └── logger.dart
│   ├── config_parser.dart  # Configuration parsing
│   └── orchestrator.dart   # Main orchestrator (_buildExecutionPlan, planFlavor, v0.4.0)
└── flutter_flavor_orchestrator.dart  # Public API

Key Components

  • FlavorOrchestrator: Coordinates the entire process; builds an ExecutionPlan before applying
  • ConfigParser: Parses and validates YAML configurations
  • AndroidProcessor: Handles Android-specific modifications
  • IosProcessor: Handles iOS-specific modifications
  • AssetProcessor: Handles file-mapping copy and planning operations
  • ExecutionPlan / PlannedOperation / OperationKind: Typed, immutable operation models with JSON serialisation
  • FileManager: Provides safe file operations with backup/rollback

📚 Examples

Check out the example directory for a complete working example with:

  • ✅ Multiple flavor configurations
  • ✅ Firebase integration
  • ✅ Custom metadata
  • ✅ Platform-specific configurations
  • ✅ Complete Flutter app
  • ✅ File mappings and safe directory replacement
  • ✅ Visual flavor verification in the running UI

Example Project Hints

The example app is designed so you can immediately see flavor changes on screen.

Quick demo in example/

cd example
flutter pub get

# Apply development flavor
flutter pub run flutter_flavor_orchestrator apply --flavor dev --verbose
flutter run

Then switch flavor and run again:

flutter pub run flutter_flavor_orchestrator apply --flavor staging --verbose
flutter run

What to look for in the UI:

  • Flavor-specific SVG icon
  • Environment/debug banner values
  • Color and typography preview from copied theme files
  • API/config values from copied lib/config/app_config.dart

Useful example references:

📖 API Documentation

Programmatic Usage

You can also use the package programmatically in your Dart code:

import 'package:flutter_flavor_orchestrator/flutter_flavor_orchestrator.dart';

void main() async {
  final orchestrator = FlavorOrchestrator(
    projectRoot: '/path/to/project',
    configPath: '/secure/jenkins/flavor_config.yaml',
    verbose: true,
  );

  // Apply a flavor
  final success = await orchestrator.applyFlavor(
    'dev',
    platforms: ['android', 'ios'],
  );

  // Preview operations without mutating files
  final plan = await orchestrator.planFlavor(
    'dev',
    platforms: ['android', 'ios'],
  );

  // List flavors
  final flavors = await orchestrator.listFlavors();

  // Validate configurations
  final valid = await orchestrator.validateConfigurations();
}

Core Classes

  • FlavorConfig: Represents a complete flavor configuration
  • ProvisioningConfig: Provisioning file configuration
  • ExecutionPlan: Ordered list of PlannedOperations for a flavor; serialisable via toJson()
  • PlannedOperation: Immutable descriptor of a single orchestration step with kind, paths, and platform
  • OperationKind: Enum — copyFile, copyDirectory, writeFile, skip
  • ConfigParser: Configuration parsing and validation
  • FlavorOrchestrator: Main orchestration logic

See the API documentation for detailed class and method documentation.

🔒 Safety Features

Automatic Backups

The orchestrator automatically creates backups of all modified files before making changes. If an error occurs, all changes are automatically rolled back.

Validation

All configurations are validated before being applied:

  • ✅ Required fields presence
  • ✅ Bundle ID format validation
  • ✅ File existence checks
  • ✅ YAML syntax validation

Error Handling

Comprehensive error handling with clear, actionable error messages:

  • 🔴 Missing configuration files
  • 🔴 Invalid bundle ID formats
  • 🔴 Missing native directories
  • 🔴 File operation failures

🧪 Testing

The package includes a comprehensive test suite:

# Run all tests
dart test

# Run with coverage
dart test --coverage

🤝 Contributing

Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • Built with ❤️ by Alessio La Mantia
  • Inspired by the need for better flavor management in Flutter projects
  • Uses excellent packages: args, yaml, xml, path

📞 Support

🗺️ Roadmap

The full roadmap is in ROADMAP.md. Here is a compact summary of progress and upcoming milestones toward the v1.0.0 stable release.

Released

Version Highlights
v0.1.x Initial CLI (apply, list, info, validate), Android/iOS processors, file mappings, dry-run, external config path
v0.2.0 Dry-run destination-presence validation, safer CI preflight
v0.3.0 OperationKind, PlannedOperation, ExecutionPlan typed models · shared _buildExecutionPlan() in orchestrator · AssetProcessor.planFileMappings()

Upcoming

Version Theme Key deliverable
v0.4.0 Preview plan command — preview operations without mutating files; planFlavor() public API; --output json
v0.5.0 Safety rollback command + timestamped backup before every non-dry-run apply
v0.6.0 Conflict protection Duplicate-target detection and --force guardrails
v0.7.0 Automation contract --output json for all commands; standardised exit codes
v0.8.0 Schema hardening schema_version enforcement, validate --strict, migration scaffold
v0.9.0 Diagnostics doctor command with categorised findings (error/warning/info)
v1.0.0 Stable Production-ready doctor, docs freeze, no breaking changes without migration path
v1.1.0 Post-1.0 Env-var interpolation (${VAR:-default}), init command

Made with ❤️ for the Flutter community

Libraries

flutter_flavor_orchestrator
Flutter Flavor Orchestrator