Skip to content

Environments

Introduction

Environment management is crucial for any production application. Bond provides a robust system for managing different environments (development, staging, production) using Flutter's --dart-define-from-file feature combined with organized configuration classes.

This approach allows you to: - Keep secrets out of your source code - Use different API endpoints for different environments - Configure feature flags per environment - Maintain consistent configuration across team members

Environment Files

Bond uses JSON files to define environment-specific configuration. These files are passed to Flutter at build time and accessed through typed configuration classes.

File Structure

my_app/
├── env.example.json     # Template with all possible keys
├── env.json            # Local development (gitignored)
├── env.staging.json    # Staging environment
└── env.production.json # Production environment

Example Environment File

env.example.json:

{
  "API_BASE_URL": "https://api.staging.myapp.com",
  "API_TIMEOUT": "30",
  "ANALYTICS_ENABLED": "true",
  "FIREBASE_PROJECT_ID": "myapp-staging",
  "SENTRY_DSN": "https://your-sentry-dsn@sentry.io/project",
  "FEATURE_SOCIAL_LOGIN": "true",
  "FEATURE_PUSH_NOTIFICATIONS": "true",
  "LOG_LEVEL": "debug"
}

env.production.json:

{
  "API_BASE_URL": "https://api.myapp.com",
  "API_TIMEOUT": "15",
  "ANALYTICS_ENABLED": "true",
  "FIREBASE_PROJECT_ID": "myapp-production",
  "SENTRY_DSN": "https://your-production-sentry-dsn@sentry.io/project",
  "FEATURE_SOCIAL_LOGIN": "true",
  "FEATURE_PUSH_NOTIFICATIONS": "true",
  "LOG_LEVEL": "error"
}

Configuration Classes

Bond organizes environment variables into typed configuration classes that provide compile-time safety and IDE autocompletion.

Core Configuration

lib/config/api.dart:

class ApiConfig {
  static String get baseUrl => env('API_BASE_URL');

  static Duration get connectTimeout => 
    Duration(seconds: int.parse(env('API_TIMEOUT')));

  static Duration get receiveTimeout => 
    Duration(seconds: int.parse(env('API_TIMEOUT')));

  static Duration get sendTimeout => 
    Duration(seconds: int.parse(env('API_TIMEOUT')));

  static bool get receiveDataWhenStatusError => true;

  static Map<String, String> get defaultHeaders => {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
  };
}

lib/config/analytics.dart:

class AnalyticsConfig {
  static bool get enabled => 
    env('ANALYTICS_ENABLED').toLowerCase() == 'true';

  static String get firebaseProjectId => env('FIREBASE_PROJECT_ID');

  static bool get crashlyticsEnabled => enabled;

  static bool get performanceMonitoringEnabled => enabled;
}

lib/config/cache.dart:

class CacheConfig {
  static Duration get defaultTtl => const Duration(minutes: 30);

  static Duration get longTtl => const Duration(hours: 24);

  static int get maxMemoryItems => 1000;

  static String get defaultStore => 'shared_preferences';

  static Map<String, dynamic> get stores => {
    'shared_preferences': {
      'driver': 'shared_preferences',
      'class': SharedPreferencesCacheDriver,
    },
    'in_memory': {
      'driver': 'in_memory',
      'class': InMemoryCacheDriver,
    },
  };
}

Feature Flags

lib/config/features.dart:

class FeatureConfig {
  static bool get socialLoginEnabled => 
    env('FEATURE_SOCIAL_LOGIN').toLowerCase() == 'true';

  static bool get pushNotificationsEnabled => 
    env('FEATURE_PUSH_NOTIFICATIONS').toLowerCase() == 'true';

  static bool get darkModeEnabled => 
    env('FEATURE_DARK_MODE', defaultValue: 'true').toLowerCase() == 'true';

  static bool get biometricAuthEnabled => 
    env('FEATURE_BIOMETRIC_AUTH', defaultValue: 'false').toLowerCase() == 'true';
}

Logging Configuration

lib/config/logging.dart:

enum LogLevel { debug, info, warning, error }

class LoggingConfig {
  static LogLevel get level {
    final levelString = env('LOG_LEVEL', defaultValue: 'info');
    switch (levelString.toLowerCase()) {
      case 'debug': return LogLevel.debug;
      case 'info': return LogLevel.info;
      case 'warning': return LogLevel.warning;
      case 'error': return LogLevel.error;
      default: return LogLevel.info;
    }
  }

  static bool get enableConsoleLogging => 
    level == LogLevel.debug || level == LogLevel.info;

  static bool get enableFileLogging => true;

  static String? get sentryDsn => 
    env('SENTRY_DSN', defaultValue: null);
}

Environment Helper Function

Bond provides a helper function to access environment variables with type safety and default values:

lib/core/utils/env.dart:

/// Get environment variable with optional default value
String env(String key, {String? defaultValue}) {
  const values = String.fromEnvironment('DART_DEFINES', defaultValue: '{}');

  try {
    final Map<String, dynamic> defines = json.decode(values);
    final value = defines[key];

    if (value == null) {
      if (defaultValue != null) {
        return defaultValue;
      }
      throw ArgumentError('Environment variable $key is required but not set');
    }

    return value.toString();
  } catch (e) {
    if (defaultValue != null) {
      return defaultValue;
    }
    throw ArgumentError('Failed to parse environment variables: $e');
  }
}

/// Get environment variable as integer
int envInt(String key, {int? defaultValue}) {
  final value = env(key, defaultValue: defaultValue?.toString());
  return int.parse(value);
}

/// Get environment variable as boolean
bool envBool(String key, {bool defaultValue = false}) {
  final value = env(key, defaultValue: defaultValue.toString());
  return value.toLowerCase() == 'true';
}

/// Get environment variable as double
double envDouble(String key, {double? defaultValue}) {
  final value = env(key, defaultValue: defaultValue?.toString());
  return double.parse(value);
}

Using Environments

Development

For local development, copy the example file and customize it:

cp env.example.json env.json
# Edit env.json with your local settings

Run with your local environment:

flutter run --flavor staging -t lib/main_staging.dart --dart-define-from-file=env.json

Staging

Use the staging environment file:

flutter run --flavor staging -t lib/main_staging.dart --dart-define-from-file=env.staging.json

Production

Use the production environment file:

flutter run --flavor production -t lib/main_production.dart --dart-define-from-file=env.production.json

IDE Configuration

VS Code Launch Configuration

.vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Development",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_staging.dart",
      "args": [
        "--flavor", "staging",
        "--dart-define-from-file=env.json"
      ]
    },
    {
      "name": "Staging",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_staging.dart",
      "args": [
        "--flavor", "staging",
        "--dart-define-from-file=env.staging.json"
      ]
    },
    {
      "name": "Production",
      "request": "launch",
      "type": "dart",
      "program": "lib/main_production.dart",
      "args": [
        "--flavor", "production",
        "--dart-define-from-file=env.production.json"
      ]
    }
  ]
}

Android Studio Run Configurations

  1. Go to RunEdit Configurations
  2. Create configurations for each environment
  3. Set Additional arguments to include the environment file

Security Best Practices

Secrets Management

Never commit sensitive data to version control:

# Add to .gitignore
env.json
env.*.json
!env.example.json

Use different secrets for each environment: - Development: Use fake/test API keys when possible - Staging: Use staging-specific credentials - Production: Use production credentials with minimal permissions

Environment Variable Validation

Add validation to ensure required variables are present:

lib/config/config_validator.dart:

class ConfigValidator {
  static void validate() {
    final requiredVars = [
      'API_BASE_URL',
      'FIREBASE_PROJECT_ID',
    ];

    final missingVars = <String>[];

    for (final varName in requiredVars) {
      try {
        env(varName);
      } catch (e) {
        missingVars.add(varName);
      }
    }

    if (missingVars.isNotEmpty) {
      throw ArgumentError(
        'Missing required environment variables: ${missingVars.join(', ')}'
      );
    }
  }
}

Call validation at app startup:

lib/app/app_run_tasks.dart:

class RunAppTasks {
  static Future<void> execute() async {
    // Validate environment configuration
    ConfigValidator.validate();

    // Other initialization tasks...
  }
}

CI/CD Integration

GitHub Actions

.github/workflows/build.yml:

name: Build and Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.10.0'

      - name: Create environment file
        run: |
          echo '${{ secrets.ENV_STAGING }}' > env.staging.json
          echo '${{ secrets.ENV_PRODUCTION }}' > env.production.json

      - name: Get dependencies
        run: flutter pub get

      - name: Run tests
        run: flutter test

      - name: Build staging
        run: flutter build apk --flavor staging -t lib/main_staging.dart --dart-define-from-file=env.staging.json

      - name: Build production
        run: flutter build apk --flavor production -t lib/main_production.dart --dart-define-from-file=env.production.json

Store your environment files as GitHub Secrets: - ENV_STAGING: Contents of env.staging.json - ENV_PRODUCTION: Contents of env.production.json

Environment-Specific Behavior

Conditional Feature Enabling

class AuthService {
  Future<void> login(String email, String password) async {
    // Use different auth endpoints based on environment
    final endpoint = FeatureConfig.socialLoginEnabled 
      ? '/auth/social-login' 
      : '/auth/basic-login';

    await apiService.post(endpoint, {
      'email': email,
      'password': password,
    });
  }
}

Debug-Only Features

class DebugService {
  static void showDebugInfo() {
    if (LoggingConfig.level == LogLevel.debug) {
      // Show debug overlay, logs, etc.
      showDebugOverlay();
    }
  }
}

Environment-Specific Styling

class AppTheme {
  static ThemeData get theme {
    return ThemeData(
      primarySwatch: Colors.blue,
      // Add debug banner in non-production environments
      debugShowCheckedModeBanner: LoggingConfig.level == LogLevel.debug,
    );
  }
}

Testing with Environments

Unit Tests

void main() {
  group('ApiConfig', () {
    test('should use correct base URL for staging', () {
      // Mock environment variables for testing
      mockEnv({'API_BASE_URL': 'https://staging.api.com'});

      expect(ApiConfig.baseUrl, equals('https://staging.api.com'));
    });
  });
}

Integration Tests

Create test-specific environment files:

env.test.json:

{
  "API_BASE_URL": "https://test.api.com",
  "ANALYTICS_ENABLED": "false",
  "LOG_LEVEL": "debug"
}

Run integration tests:

flutter test integration_test/ --dart-define-from-file=env.test.json

Troubleshooting

Common Issues

Environment variables not found:

// Always provide sensible defaults
static String get baseUrl => env('API_BASE_URL', defaultValue: 'https://localhost:3000');

JSON parsing errors:

# Validate your JSON files
cat env.json | jq .

Build failures with environment files:

# Ensure the file exists and is valid JSON
ls -la env*.json
flutter clean && flutter pub get

Debugging Environment Issues

Add logging to see what environment variables are loaded:

void debugEnvironment() {
  if (LoggingConfig.level == LogLevel.debug) {
    print('API Base URL: ${ApiConfig.baseUrl}');
    print('Analytics Enabled: ${AnalyticsConfig.enabled}');
    print('Feature Flags: Social Login=${FeatureConfig.socialLoginEnabled}');
  }
}

Next Steps

Now that you understand environment management in Bond:

  1. Set up Firebase - Configure Firebase for each environment
  2. Learn about Flavors - Understand how flavors work with environments
  3. Explore Service Providers - See how configuration is used in providers
  4. Set up CI/CD - Automate builds with proper environment handling

Best Practices Summary

  • ✅ Use typed configuration classes instead of raw environment access
  • ✅ Provide sensible defaults for non-critical settings
  • ✅ Validate required environment variables at startup
  • ✅ Keep secrets out of version control
  • ✅ Use different configurations for each environment
  • ✅ Document all environment variables in the example file
  • ✅ Test your app with different environment configurations