利用 Dart-Define 實現 Flutter 多環境配置

WzDTj發表於2021-03-06

背景

現在常規的專案基本都有 2 個及以上的環境。為區分這些環境就需要對應的配置檔案,我們需要它們來控制 API 地址、應用名稱、版本號等資訊。

在 Flutter 1.17 中,Flutter 會將在編譯前將 dart-defines 傳遞給 iOS、Android、macOS。
因此,可以幾乎在各個時刻呼叫預先定義的 dart-defines。

目標

  • 一處配置,多端使用。
  • 熟悉的配置模式。採用 .env.* 模式來提供配置。
  • 不入侵專案程式碼。
  • 沒有第三方依賴。

來!開始倒騰吧!

先來看看 --dart-define 的官方說明

dart-define=<foo=bar>
Additional key-value pairs that will be available as constants from the String.fromEnvironment, bool.fromEnvironment, int.fromEnvironment, and double.fromEnvironment constructors.

利用 --dart-define 可以預先定義常量

flutter run --dart-define=APP_NAME=example_app --dart-define=USER_NAME=WzDTj

配置太多怎麼辦?

配置太多當然就寫到配置檔案中去呀!

// .env.local
APP_NAME=example_app_local
USER_NAME=WzDTj
// .env.staging
APP_NAME=example_app_staging
USER_NAME=WzDTj
// .env.production
APP_NAME=example_app_production
USER_NAME=WzDTj

可以指定配置檔案嗎?

寫好配置檔案,自然希望可以在命令中指定該檔案。

遺憾的是官方並沒有提供相關方法。

Github 上有人提議希望支援 .env,可惜官方並不願意。

既然官方不支援,那就自己寫個指令碼實現唄。

// flutter_tool.sh

#!/bin/bash

for i in "$@"
do
case $i in
    --model=*)
    MODEL="${i#*=}"
    ;;
    *)
        # unknown option
    ;;
esac
done

VARS=( $(cut -d ' ' -f1 .env.$MODEL) )

DART_DEFINES=""
for (( i = 0; i < ${#VARS[@]}; i++ )); do
    DART_DEFINES+=" --dart-define=${VARS[i]}"
done

flutter $1 $DART_DEFINES

執行指令碼

./flutter_tool run --model=local

如何使用定義好的 dart-define 呢?

在專案程式碼中直接呼叫 fromEnvironment 方法。

// *.dart
const appName = String.fromEnvironment("APP_NAME", defaultValue: "default ppp name");

類似的還有:

  • String.fromEnvironment
  • bool.fromEnvironment
  • int.fromEnvironment

怎麼控制 iOS 專案配置的資訊呢?

當我們用 Flutter 編譯 iOS 端時,會在 ./ios/Flutter 目錄下生成兩個檔案 Generated.xcconfigflutter_export_environment.sh

其中我們會看到之前設定的 dart-define。

// Generated.xcconfig
// ...
DART_DEFINES=APP_NAME%3Dexample_app_local,USER_NAME%3DWzDTj
// ...
// flutter_export_environment.sh
// ...
export "DART_DEFINES=APP_NAME%3Dexample_app_local,USER_NAME%3DWzDTj"
// ...

先來說下思路。

我們將 DART_DEFINES 解析到某個 .xcconfig 中,再通過引入該檔案來達到我們想要的目的。

首先需配置 iOS 專案的 Scheme
將以下指令碼程式碼輸入到 Build -> Pre-actions

function urldecode() { : "${*//+/ }"; echo "${_//%/\\x}"; }

IFS=',' read -r -a define_items <<< "$DART_DEFINES"

for index in "${!define_items[@]}"
do
    define_items[$index]=$(urldecode "${define_items[$index]}");
done

printf "%s\n" "${define_items[@]}" > ${SRCROOT}/Flutter/DartDefines.xcconfig

這樣就可以在編譯前得到一份 DartDefines.xcconfig 檔案。

然後,在 Debug.xcconfigRelease.xcconfig 中引入該檔案。

// ./ios/Flutter/Debug.xcconfig

#include "Generated.xcconfig"
#include "DartDefines.xcconfig"
// ./ios/Flutter/Release.xcconfig

#include "Generated.xcconfig"
#include "DartDefines.xcconfig"

最後,按需修改下 Info.plist 就行了。

例如:控制應用名稱。

// ./ios/Runner/Info.plist
// ...
<key>CFBundleName</key>
<string>$(APP_NAME)</string>
// ...

怎麼控制 Android 專案配置的資訊呢?

Flutter 會將所有的 dart-define 用逗號連結成一個字串傳遞給 Gradle。

// ./android/app/build.gradle

println(project.property('dart-defines'));

// Flutter v1.17 Output: APP_NAME=example_app_local,USER_NAME=WzDTj
// Flutter v1.20 Output: APP_NAME%3Dexample_app_local,USER_NAME%3DWzDTj

// 解析該字串
def dartDefines = []
if (project.hasProperty('dart-defines')) {
    dartDefines = project.property('dart-defines')
            .split(',')
            .collectEntries { entry ->
                def pair = URLDecoder.decode(entry, StandardCharsets.UTF_8.toString()).split('=')
                [(pair.first()): pair.last()]
            }
}

得到 dartDefines 後,便可按需配置了。

例如:定義一個字串資源供 AndroidManifest.xml 呼叫。

// ./android/app/build.gradle

defaultConfig {
    // ...
    resValue "string", "app_name", dartDefines.APP_NAME
}
// ./android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.flutter_app">
   <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher">
        // ...
    </application>
</manifest>

用 Android Studio 開發怎麼辦呢?

可以將上述指令碼中 DART_DEFINES 變數列印出來,填入 Run/Debug Configurations 中的 Additional arguments 即可。

參考資料

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章