Mastering Environment Management in Dart/Flutter
A robust pattern for multi-environment (Dev, Staging, Prod) configuration.
Managing different environments—Development, Staging, and Production—is crucial for any serious Flutter application. We need a way to reliably switch configurations, such as API base URLs, feature flags, and logging settings, based on the application flavor we build. This guide walks you through setting up a clean, scalable, and type-safe flavor management system in Dart.
1. Define the Flavors (`flavor_enum.dart`)
The cornerstone of the system is the Flavor enum. It explicitly defines all possible environments and includes a crucial static factory, fromString, to safely convert a raw string (like a build argument) into a type-safe enum value.
/// flavor_enum.dart
library;
enum Flavor {
development,
production,
staging;
static Flavor fromString(String? value) {
return Flavor.values.firstWhere(
(e) => e.name == value?.toLowerCase(),
orElse: () => Flavor.production,
);
}
}
2. Determine the Build Mode (`flavor/build_mode/build_mode.dart`)
While flavors handle environment configuration, Build Mode (debug, profile, release) handles Dart-specific compilation contexts. This file gives us a clear, static way to know the mode.
import 'package:flutter/foundation.dart';
enum BuildMode {
debug,
profile,
release;
static BuildMode get current {
if (kDebugMode) return BuildMode.debug;
if (kProfileMode) return BuildMode.profile;
if (kReleaseMode) return BuildMode.release;
throw UnimplementedError('Unknown build mode');
}
}
3. The `FlavorConfig` Interface (`flavor/config/config.dart`)
The `FlavorConfig` abstract interface defines what every environment configuration must provide (like the baseUrl). The factory constructor is the central piece of logic, instantiating the correct configuration based on the provided flavor string.
part of '../flavor.dart';
abstract interface class FlavorConfig {
const FlavorConfig._(this.baseUrl, this.flavor);
final String baseUrl;
final Flavor flavor;
factory FlavorConfig({String? flavorName = appFlavor}) {
final flavor = Flavor.fromString(flavorName);
switch (flavor) {
case Flavor.development:
return const _DevCfg();
case Flavor.production:
return const _ProdCfg();
case Flavor.staging:
return const _StgCfg();
}
}
BuildMode get buildMode => BuildMode.current;
}
4. Implementing the Specific Configurations
For each flavor, we create a concrete implementation of FlavorConfig, which simply sets the unique properties for that environment.
Development Config
part of '../../flavor.dart';
class _DevCfg extends FlavorConfig {
const _DevCfg()
: super._('https://api.dev.yourdomain.com', Flavor.development);
}
Staging Config
part of '../../flavor.dart';
class _StgCfg extends FlavorConfig {
const _StgCfg() : super._('https://api.stg.yourdomain.com', Flavor.staging);
}
Production Config
part of '../../flavor.dart';
class _ProdCfg extends FlavorConfig {
const _ProdCfg()
: super._('https://api.prod.yourdomain.com', Flavor.production);
}
5. The Main Flavor File (`flavor.dart`)
Finally, we create a central file that defines the library structure and exports everything needed for consumption throughout the application.
export 'package:flutter/services.dart' show appFlavor;
import 'build_mode/build_mode.dart' show BuildMode;
import 'flavor_enum.dart' show Flavor;
import 'package:flutter/services.dart' show appFlavor;
part 'config/config.dart';
part 'config/dev/dev_cfg.dart';
part 'config/prod/prod_cfg.dart';
part 'config/stg/stg_cfg.dart';
6. Automating Configuration and Running the App
The modular code structure demonstrated above is part of a larger project, which you can explore on GitHub: https://github.com/mg3994/mono-modular-fltr.git
Simplifying Setup with `flutter_flavorizr`
Setting up native files manually for each flavor is repetitive. Use flutter_flavorizr — it automates native flavor setup.
Android Example
android {
flavorDimensions += "default"
productFlavors {
create("development") {
dimension = "default"
applicationIdSuffix = ".dev"
resValue("string", "app_name", "MyApp DEV")
}
create("staging") {
dimension = "default"
applicationIdSuffix = ".stg"
resValue("string", "app_name", "MyApp STAGING")
}
create("production") {
dimension = "default"
resValue("string", "app_name", "MyApp")
}
}
}
Run Command
flutter run --flavor staging
The selected flavor name (e.g., staging) is automatically passed to Dart via appFlavor.
No comments:
Post a Comment