Configuring Different Deployment Targets (App Store, Play Store, Web) in Flutter

Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, is incredibly versatile. A common requirement in app development is the ability to configure different deployment targets (e.g., App Store, Google Play Store, and web) from the same codebase. This ensures that your app behaves differently or has unique configurations based on the platform it is deployed to. This blog post will explore the various ways to achieve this in Flutter.

Understanding Deployment Targets

Before diving into the technical details, let’s define our deployment targets:

  • App Store (iOS): Applications deployed to Apple’s App Store.
  • Google Play Store (Android): Applications deployed to the Google Play Store.
  • Web (Browser): Applications deployed as web apps running in a browser.

Each deployment target might require different configurations such as:

  • Different app icons and splash screens.
  • Unique API endpoints for development, staging, and production environments.
  • Platform-specific feature flags or settings.
  • Custom build configurations (e.g., code signing identities for iOS).

Methods to Configure Different Deployment Targets

1. Using Flutter Flavors

Flutter Flavors (also known as build variants) allow you to build different versions of your app from the same codebase. This is achieved through build configurations in Gradle (for Android) and Xcode (for iOS).

Step 1: Setting up Flavors in Android

Modify your android/app/build.gradle file to define product flavors:


android {
    ...
    flavorDimensions "default"
    productFlavors {
        dev {
            dimension "default"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
            resValue "string", "app_name", "MyApp Dev"
            buildConfigField "String", "API_URL", ""https://dev.example.com/api/""
        }
        prod {
            dimension "default"
            resValue "string", "app_name", "MyApp"
            buildConfigField "String", "API_URL", ""https://example.com/api/""
        }
    }
    ...
}

In this configuration:

  • We define two flavors: dev and prod.
  • applicationIdSuffix and versionNameSuffix modify the application ID and version name for the dev flavor.
  • resValue allows you to override resources like app_name.
  • buildConfigField defines build configuration fields like API_URL that can be accessed in your Dart code.
Step 2: Setting up Schemes in iOS

In Xcode, go to Product > Scheme > Edit Scheme. Duplicate the existing scheme and configure it for your different environments.

Xcode Edit Scheme

Add configurations for each flavor using build configurations. Each scheme will point to different .xcconfig files.

// ios/Flutter/dev.xcconfig
#include "Generated.xcconfig"
APP_DISPLAY_NAME = MyApp Dev
API_URL = https://dev.example.com/api/
// ios/Flutter/prod.xcconfig
#include "Generated.xcconfig"
APP_DISPLAY_NAME = MyApp
API_URL = https://example.com/api/
Step 3: Accessing Flavor-Specific Variables in Flutter

For Android, access the API_URL using the BuildConfig class:


import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  String apiUrl = const String.fromEnvironment('API_URL', defaultValue: 'https://default.example.com/api/');
  
  if (kDebugMode) {
    print('API URL: $apiUrl');
  }
  
  runApp(MyApp(apiUrl: apiUrl));
}

class MyApp extends StatelessWidget {
  final String apiUrl;
  
  const MyApp({Key? key, required this.apiUrl}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Flavors Example'),
        ),
        body: Center(
          child: Text('API URL: $apiUrl'),
        ),
      ),
    );
  }
}

For iOS, access variables defined in .xcconfig files:


import 'package:flutter/services.dart';

Future getApiUrl() async {
  const MethodChannel methodChannel = MethodChannel('flavorConfig');
  try {
    final String result = await methodChannel.invokeMethod('getApiUrl');
    return result;
  } on PlatformException catch (e) {
    print("Failed to get API URL: '${e.message}'.");
    return 'https://default.example.com/api/';
  }
}

And the corresponding platform-specific code (AppDelegate.swift for iOS):


import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let flavorChannel = FlutterMethodChannel(name: "flavorConfig",
                                              binaryMessenger: controller.binaryMessenger)
    flavorChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      if call.method == "getApiUrl" {
        result(Bundle.main.infoDictionary?["ApiUrl"] as? String)
      } else {
        result(FlutterMethodNotImplemented)
      }
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
Step 4: Building Flavored Apps

To build the flavored apps:

  • For Android: flutter build apk --flavor dev or flutter build apk --flavor prod
  • For iOS: Select the correct scheme in Xcode and build the app.

2. Using Environment Variables

Another way to configure different targets is by using environment variables. This method is useful for setting dynamic configurations without modifying the code.

Step 1: Set Environment Variables

Set environment variables specific to each environment.

For Linux/macOS:


export API_URL=https://dev.example.com/api/

For Windows:


$env:API_URL="https://dev.example.com/api/"
Step 2: Accessing Environment Variables in Flutter

Use the dart:io package to access these variables:


import 'dart:io';

void main() {
  String apiUrl = Platform.environment['API_URL'] ?? 'https://default.example.com/api/';
  print('API URL: $apiUrl');
  runApp(MyApp(apiUrl: apiUrl));
}
Step 3: Deployment

Make sure to configure these variables on your deployment server or environment. For example, in a CI/CD pipeline or web server configuration.

3. Using Separate Configuration Files

A more structured approach is to use separate configuration files (e.g., config.dev.json, config.prod.json) for different environments.

Step 1: Create Configuration Files

Create different JSON configuration files for each environment.


// config.dev.json
{
  "api_url": "https://dev.example.com/api/",
  "app_name": "MyApp Dev"
}

// config.prod.json
{
  "api_url": "https://example.com/api/",
  "app_name": "MyApp"
}
Step 2: Load Configuration Files

Load these files during app initialization based on the deployment environment.


import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;

class AppConfig {
  final String apiUrl;
  final String appName;

  AppConfig({required this.apiUrl, required this.appName});

  static Future fromEnvironment(String environment) async {
    try {
      final String jsonString = await rootBundle.loadString('config.$environment.json');
      final Map configData = json.decode(jsonString) as Map;

      return AppConfig(
        apiUrl: configData['api_url'] as String,
        appName: configData['app_name'] as String,
      );
    } catch (e) {
      print('Error loading config: $e');
      return AppConfig(
        apiUrl: 'https://default.example.com/api/',
        appName: 'Default App',
      );
    }
  }
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  const environment = String.fromEnvironment('APP_ENVIRONMENT', defaultValue: 'dev');
  final config = await AppConfig.fromEnvironment(environment);

  runApp(MyApp(config: config));
}

class MyApp extends StatelessWidget {
  final AppConfig config;

  const MyApp({Key? key, required this.config}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: config.appName,
      home: Scaffold(
        appBar: AppBar(
          title: Text(config.appName),
        ),
        body: Center(
          child: Text('API URL: ${config.apiUrl}'),
        ),
      ),
    );
  }
}
Step 3: Building and Deploying

When building, set the APP_ENVIRONMENT variable to choose the correct configuration.


flutter run --dart-define=APP_ENVIRONMENT=prod

Conclusion

Configuring different deployment targets in Flutter can be achieved using various methods like Flutter Flavors, Environment Variables, and separate Configuration Files. Flutter Flavors are powerful for building platform-specific apps with unique settings managed through Gradle and Xcode. Environment Variables provide a dynamic way to set configurations during runtime, and Separate Configuration Files offer a structured method using JSON files loaded during app initialization.

Choose the method that best suits your project’s needs and complexity. Properly configured deployment targets ensure that your Flutter app behaves as expected across all platforms and environments, streamlining development, testing, and deployment processes.