DEV Community

nrikiji
nrikiji

Posted on • Edited on

What I always do when I develop in flutter

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

#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = nrikiji.flutter-start-app.dev
DISPLAY_NAME = Debug StartApp
Enter fullscreen mode Exit fullscreen mode

ios/Flutter/Release.xcconfig

#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = nrikiji.flutter-start-app
DISPLAY_NAME = StartApp
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

before
before

after
after

Android

Preparing signings for production

.gitignore

# Android Signing
android/app/signing/key.jks
android/app/signing/signing.gradle
Enter fullscreen mode Exit fullscreen mode

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
Enter fullscreen mode Exit fullscreen mode

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"
    ・・・
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

.gitignore

# Firebase Settings Files
ios/Runner/GoogleService-Info-dev.plist
ios/Runner/GoogleService-Info-prod.plist
Enter fullscreen mode Exit fullscreen mode

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

script

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

.gitignore

# Firebase Settings Files
android/app/src/debug/google-services.json
android/app/src/release/google-services.json
Enter fullscreen mode Exit fullscreen mode

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'
    ・・・
    }
}
Enter fullscreen mode Exit fullscreen mode

android/app/build.gradle

・・・
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
Enter fullscreen mode Exit fullscreen mode

Initializing firebase in flutter

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();
      }
        });
  }
}
Enter fullscreen mode Exit fullscreen mode

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

release: set "--release --dart-define env=prod" to "Addional arguments"
release

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
Enter fullscreen mode Exit fullscreen mode

Constant definitions for each environment

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,
}
Enter fullscreen mode Exit fullscreen mode

4. Localization

pubspec.yaml

dependencies:
  ・・・
  flutter_localizations:
    sdk: flutter
Enter fullscreen mode Exit fullscreen mode

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',
      );
}
Enter fullscreen mode Exit fullscreen mode

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

fastlane
Gemfile
ios/fastlane/Appfile
ios/fastlane/Fastfile

GitHub Actions secrets
this url

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/

Top comments (0)