Perché servono ambienti multipli

Quando un'app Flutter cresce, è indispensabile poter distinguere tra diversi ambienti di esecuzione: sviluppo (dev), staging (collaudo) e produzione (prod). Ogni ambiente ha tipicamente:

  • Un endpoint API differente (api.dev.miaapp.com vs api.miaapp.com)
  • Un applicationId / bundle identifier diverso, così da installare più versioni sullo stesso dispositivo
  • Nomi e icone distinti per riconoscere a colpo d'occhio quale build stai usando
  • Chiavi di analytics o crash reporting separate

In Flutter questa separazione si ottiene combinando i flavor nativi (Android) e le build configuration/scheme (iOS) con una configurazione a livello Dart tramite gli entry point e i dart-define.

Configurare i flavor su Android

Apri android/app/build.gradle e aggiungi i productFlavors all'interno del blocco android:

android {
    // ...
    flavorDimensions "env"

    productFlavors {
        dev {
            dimension "env"
            applicationIdSuffix ".dev"
            resValue "string", "app_name", "MiaApp Dev"
        }
        staging {
            dimension "env"
            applicationIdSuffix ".staging"
            resValue "string", "app_name", "MiaApp Staging"
        }
        prod {
            dimension "env"
            resValue "string", "app_name", "MiaApp"
        }
    }
}

Nel AndroidManifest.xml usa la risorsa dinamica per il nome dell'app:

<application
    android:label="@string/app_name"
    ... >

Grazie all'applicationIdSuffix potrai avere com.miaazienda.miaapp.dev, .staging e la versione di produzione installate contemporaneamente.

Configurare le scheme su iOS

Su iOS il processo è più manuale. In Xcode:

  1. Duplica la configurazione Debug, Release e Profile per ogni ambiente (es. Debug-dev, Release-prod, ecc.).
  2. Crea uno Scheme per ogni flavor tramite Product > Scheme > Manage Schemes.
  3. Definisci una User-Defined Setting (es. APP_NAME e BUNDLE_SUFFIX) e usala nel Info.plist con $(APP_NAME).

Imposta il PRODUCT_BUNDLE_IDENTIFIER di ogni configurazione, ad esempio com.miaazienda.miaapp.dev per l'ambiente di sviluppo.

Gli entry point Dart

Crea un file di configurazione condiviso e più main distinti. Prima definiamo la configurazione:

// lib/config/app_config.dart
enum Environment { dev, staging, prod }

class AppConfig {
  final Environment environment;
  final String apiBaseUrl;
  final String appName;

  const AppConfig({
    required this.environment,
    required this.apiBaseUrl,
    required this.appName,
  });

  static late AppConfig instance;
}

Ora gli entry point, uno per ambiente:

// lib/main_dev.dart
import 'package:flutter/material.dart';
import 'config/app_config.dart';
import 'app.dart';

void main() {
  AppConfig.instance = const AppConfig(
    environment: Environment.dev,
    apiBaseUrl: 'https://api.dev.miaapp.com',
    appName: 'MiaApp Dev',
  );
  runApp(const MyApp());
}
// lib/main_prod.dart
import 'package:flutter/material.dart';
import 'config/app_config.dart';
import 'app.dart';

void main() {
  AppConfig.instance = const AppConfig(
    environment: Environment.prod,
    apiBaseUrl: 'https://api.miaapp.com',
    appName: 'MiaApp',
  );
  runApp(const MyApp());
}

Nel resto del codice accedi alla configurazione tramite AppConfig.instance.apiBaseUrl.

Eseguire e compilare i flavor

Per avviare o compilare un flavor specifico usa i flag --flavor e --target:

# Avvio in sviluppo
flutter run --flavor dev --target lib/main_dev.dart

# Build di produzione (Android)
flutter build appbundle --flavor prod --target lib/main_prod.dart

# Build iOS di staging
flutter build ipa --flavor staging --target lib/main_staging.dart

Alternativa: --dart-define

Se preferisci evitare più file main, puoi passare variabili a compile-time con --dart-define:

flutter run --dart-define=ENV=dev --dart-define=API_URL=https://api.dev.miaapp.com

E leggerle in Dart:

const env = String.fromEnvironment('ENV', defaultValue: 'dev');
const apiUrl = String.fromEnvironment('API_URL');

Per gestire molte variabili conviene usare un file JSON con --dart-define-from-file=config/dev.json, supportato dalle versioni recenti di Flutter.

Automatizzare con la configurazione dell'IDE

In VS Code definisci le configurazioni in .vscode/launch.json:

{
  "configurations": [
    {
      "name": "Dev",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_dev.dart",
      "args": ["--flavor", "dev"]
    },
    {
      "name": "Prod",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_prod.dart",
      "args": ["--flavor", "prod"]
    }
  ]
}

Best practice

  • Non committare segreti: mantieni chiavi API e credenziali fuori dal repository, usando file di configurazione ignorati o secret manager della CI.
  • Un solo punto di verità: centralizza la configurazione in AppConfig invece di sparpagliare condizioni if (env == ...) nel codice.
  • Icone differenti: usa flutter_launcher_icons con override per flavor per generare icone diverse e riconoscere subito la build.
  • Integra la CI/CD: parametrizza i job di build in base al flavor per generare artifact separati per ogni ambiente.

Conclusione

I flavor sono la soluzione standard per gestire ambienti multipli in Flutter. Con una configurazione iniziale ben strutturata — flavor nativi, entry point Dart e una classe AppConfig centralizzata — potrai passare da sviluppo a produzione con un solo comando, riducendo errori e velocizzando il ciclo di rilascio.