What I always do when I develop in flutter
Tags: flutter, mobile, ios, android
Published: 2021-11-02 16:03:50

## 1. Switch Bundle ID and app name between production and development versions
We want to have a production and a development version of the app living together on one device. Also, I want to use the development version for debug builds and the production version for release builds. iOS and Android.

**iOS**.
Launch IOS/Runner.xcworkspace in xcode

ios/Flutter/Debug.xcconfig
```bash:
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = nrikiji.flutter-start-app.dev
DISPLAY_NAME = Debug StartApp
```

ios/Flutter/Release.xcconfig
```
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = nrikiji.flutter-start-app
DISPLAY_NAME = StartApp
```

Apply the app name in xcconfig

ios/Runner/Info.plist
```
<?xml version="1.0" encoding="UTF-8"? >
...
<dict> ...
...
  <key>CFBundleDisplayName</key
  <string>$(DISPLAY_NAME)</string>
```

Delete the Runner column of Product Bundle Identifier in TARGETS > Runner > Build Settings (delete button) to reflect the Bundle ID in xcconfig.

before
![before](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2v5phl28k5g6urke2ziq.png)
 
after
![after](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zmm8etfw33uziszxtx0l.png)
 
**Android**

Preparing signings for production

.gitignore
```
# Android Signing
android/app/signing/key.jks
android/app/signing/signing.gradle
```

Add the JKS file to the project
android/app/signing/key.jks

android/app/signing/signing.gradle
```
signingConfigs {
    release {
        storeFile file("key.jks")
        storePassword "xxxxx"
        keyAlias "xxxxx"
        keyPassword "xxxxx"
    }
}
```

Set Bundle ID, app name and signing for production build

android/app/build.gradle
```
android {
    ・・・
    defaultConfig {
        ・・・
        applicationId "nrikiji.flutter_start_app"
        ・・・
    }

    buildTypes {
        debug {
            debuggable true
            applicationIdSuffix ".dev"
            resValue "string", "app_name", "Debug StartApp"
        }
        release {
            debuggable false
            applicationIdSuffix ""
            resValue "string", "app_name", "StartApp"

            apply from: './signing/signing.gradle', to: android
            signingConfig signingConfigs.release
        }
    }
```

Make sure to use the app name you set above.

android/app/src/main/AndroidManifest.xml
```
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="nrikiji.flutter_start_app">
   <application
        android:label="@string/app_name"
	・・・
```

## 2. Switch firebase configuration file between production and development versions

I want to use firebase analytics and crashlytics as a minimum in any app. I also assume that you have already created a firebase project and app. iOS, Android and flutter.

**iOS**
Download GoogleService-Info.plist from firebase console and place it under IOS/Runner. Use different file names for development and production.
```
$ ls -l 
ios/Runner/GoogleService-Info-dev.plist
ios/Runner/GoogleService-Info-prod.plist
```

.gitignore
```
# Firebase Settings Files
ios/Runner/GoogleService-Info-dev.plist
ios/Runner/GoogleService-Info-prod.plist
```

Configure Run Script in TARGETS > Runner > Build Phases to develop GoogleService-Info.plist and switch it in production build.

![script](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iul01aqay6lfv068big2.png)

script
```
if [ "${CONFIGURATION}" == "Debug" ]; then
cp -r "${PROJECT_DIR}/Runner/GoogleService-Info-dev.plist" "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/GoogleService-Info.plist"
elif [ "${CONFIGURATION}" == "Release" ]; then
cp -r "${PROJECT_DIR}/Runner/GoogleService-Info-prod.plist" "$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/GoogleService-Info.plist"
fi
```

**Android**
Download google-services.json from firebase console and place it under debug and release (create directories) under android/app/src.

```
$ ls -l 
android/app/src/debug/google-services.json
android/app/src/release/google-services.json
```

.gitignore
```
# Firebase Settings Files
android/app/src/debug/google-services.json
android/app/src/release/google-services.json
```

Add the gradle plugin

android/build.gradle
```
buildscript {
    ・・・
    dependencies {
        classpath 'com.google.gms:google-services:4.3.4'
        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.5.1'
	・・・
    }
}
```

android/app/build.gradle
```
・・・
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
```

**Initializing firebase in flutter**
```dart:lib/main.dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';

void main() {
  runZonedGuarded(
    () {
      WidgetsFlutterBinding.ensureInitialized();
      return runApp(ProviderScope(child: MyApp()));
    },
    (e, st) {
      FirebaseCrashlytics.instance.recordError(e, st);
    },
  );
}

class MyApp extends StatelessWidget {
  final _initialization = Firebase.initializeApp();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: _initialization,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.done || snapshot.hasError) {
            return MaterialApp(・・・);
          } else {
            return SizedBox.shrink();
	  }
        });
  }
}
```

## 3. Switch the constants used in the program between the production and development versions.

I want to use different URLs for Web API connections in the production and development versions. Also, I want to use development constants for debug builds and production constants for release builds.

**AndroidStudio Build Settings**
Add debug and release from "Edit Configurations" in Android Studio.

debug: set "--dart-define env=dev" to "Addional arguments"
![debug](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tcgry3uw1b1c4galvphj.png)

release: set "--release --dart-define env=prod" to "Addional arguments"
![release](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ks17kclkzf8t3mi6timu.png)
 
When building from the command line
```
# Debug
$ flutter build ios --dart-define=env=dev

# Release(iOS)
$ flutter build ios --release --dart-define=env=prod

# Release(Android)
$ flutter build appbundle --release --dart-define=env=prod --no-shrink
```

**Constant definitions for each environment**
```dart:lib/environment.dart
class Environment {
  final EnvironmentKind kind;
  final String baseApiUrl;

  factory Environment() {
    const env = String.fromEnvironment('env');
    return env == 'prod' ? Environment.prod() : Environment.dev();
  }

  const Environment._({
    this.kind,
    this.baseApiUrl,
  });

  const Environment.prod()
      : this._(
          kind: EnvironmentKind.Prod,
          baseApiUrl: "https://example.com",
        );

  const Environment.dev()
      : this._(
          kind: EnvironmentKind.Dev,
          baseApiUrl: "https://dev.example.com",
        );
}

enum EnvironmentKind {
  Dev,
  Prod,
}
```

## 4. Localization
pubspec.yaml
```
dependencies:
  ・・・
  flutter_localizations:
    sdk: flutter
```

lib/localize.dart
```
import 'package:flutter/cupertino.dart';

class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
  const AppLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => ['en', 'ja'].contains(locale.languageCode);

  @override
  Future<AppLocalizations> load(Locale locale) async => AppLocalizations(locale);

  @override
  bool shouldReload(AppLocalizationsDelegate old) => false;
}

class AppLocalizations {
  final LocalizeMessages messages;

  AppLocalizations(Locale locale) : this.messages = LocalizeMessages.of(locale);

  static LocalizeMessages of(BuildContext context) {
    return Localizations.of<AppLocalizations>(context, AppLocalizations)!.messages;
  }
}

class LocalizeMessages {
  final String greet;

  LocalizeMessages({
    required this.greet,
  });

  factory LocalizeMessages.of(Locale locale) {
    switch (locale.languageCode) {
      case 'ja':
        return LocalizeMessages.ja();
      case 'en':
        return LocalizeMessages.en();
      default:
        return LocalizeMessages.en();
    }
  }

  factory LocalizeMessages.ja() => LocalizeMessages(
        greet: 'こんにちは',
      );

  factory LocalizeMessages.en() => LocalizeMessages(
        greet: 'Hello',
      );
}
```

## 5. Binary upload to store with GitHub Actions
Once I learned to prevent build errors, I became lazy to upload manually. Also, the App Store Connect API and Google Play Developer Publishing API should be enabled.

Prepare the following file and set the necessary values in the GitHub Actions secrets so that the workflow will be executed when the git tag is pushed.

**workflow**
[.github/workflows/release.yml](https://github.com/nrikiji/flutter_starter/blob/master/.github/workflows/release.yml)

**fastlane**
[Gemfile](https://github.com/nrikiji/flutter_starter/blob/master/ios/Gemfile)
[ios/fastlane/Appfile](https://github.com/nrikiji/flutter_starter/blob/master/ios/fastlane/Appfile)
[ios/fastlane/Fastfile](https://github.com/nrikiji/flutter_starter/blob/master/ios/fastlane/Fastfile)

**GitHub Actions secrets**
[this url](https://github.com/nrikiji/flutter_starter/blob/master/README.en.md#configure-githubactions)

## Summary
This is a startup project that you can start by git cloning what I wrote here. The usage procedure is summarized in the README.

https://github.com/nrikiji/flutter_starter/