🔨

【Flutter 3.19対応】Dart-define-from-fileを使って開発環境と本番環境を分ける

に公開
41
GitHubで編集を提案
Altiveエンジニアリングブログ

Discussion

monomono

Flutter SDKのバージョンアップグレードで Dart-defines の暗号化方法が変わり、対応の必要が出てくる可能性がある

暗号化ではなく、エンコードです。
秘匿する目的ではなく、渡されたパラメーターの値を後から正確に展開するための処理のはずです。
暗号化と表現すると、ここにセキュアな値を入れても良いかのような誤解を招きそうですが、そうではないので。

akaboshinitakaboshinit

記事わかりやすく、とても参考になりました!
ありがとうございます🙇

自分が利用していて気づいたことなのですが
このDart-defineを使った上でfirebaseを新規導入する際は
Copy Bundle ResourcesにSelect GoogleService-Info.plistで
コピーされたGoogleService-Info.plistを追加しないと
GoogleService-Info.plistが読み込まれずfirebaseを使うことができないみたいです!
xcodeconfig

村松龍之介村松龍之介

@akaboshinit さん、追加情報ありがとうございます✨
遅くなってしまいましたが、記事に追記させていただきました🙌

ツルオカツルオカ

Flavor対応参考にさせていただきました。
--dart-defineが出る前の名残でflutter_flavorizr | Flutter Package辺りを使って凌いでましたが、記事の通り進めたらさくっとできました。ありがとうございます。

1点アプリアイコンの切り替えについてですが、出力までの手順に留まっており反映の記述が漏れている様子でしたので、間違いなさそうであれば追記お願いします🙏
(私の環境ではこちらの設定なしではアイコン反映できずでした)

村松龍之介村松龍之介

ツルオカさん、ご報告ありがとうございます🙏😊
後ほど追記させていただきます!

kboy (Kei Fujikawa)kboy (Kei Fujikawa)

Xcode上でRunnerディレクトリへ GoogleService-Info.plist ファイル(どの環境のものでも良い)をドラッグ&ドロップで追加してファイルと参照を追加しましょう。
Downloadフォルダ等から、Runnerフォルダ内へドラッグ&ドロップ (Copy items if needed にチェック)

ここに関してのコメントですが、Downloadフォルダ等からではなく、Runnerディレクトリにコピーされて出来上がったものをドラッグアンドドロップして参照するようにするのが良いと思います!(ダウンロードフォルダからとかだと参照のディレクトリが違くて、「Cannnot find ~/GoogleService-Info.plist」みたいなエラーが出そうです!)

村松龍之介村松龍之介

kboyさん、コメントありがとうございます😊
Xcodeでドラッグ&ドロップする際に、 Copy items if needed にチェックすれば、Downloadフォルダにあるファイルへの参照ではなく、新規にファイルを作成して配置してくれるので参照エラーは出ない認識でした…!
試してみて問題あるようだったら修正しますね💪

kboy (Kei Fujikawa)kboy (Kei Fujikawa)

Xcodeでドラッグ&ドロップする際に、 Copy items if needed にチェックすれば、Downloadフォルダにあるファイルへの参照ではなく、新規にファイルを作成して配置してくれるので参照エラーは出ない認識でした…!

なるほどです!そしたらコピーされるので大丈夫かもです!!

Daigo WakabayashiDaigo Wakabayashi

Flavor対応参考にさせて頂きました〜!
記事通り進めるだけで手軽にできて驚きました、ありがとうございます!!

Android Studio の設定例

TODO

もし良ければスクラップに残しておいたので使ってください!🎯

https://zenn.dev/mamushi/scraps/13c0232c88227e

村松龍之介村松龍之介

Daigoさん、サポートありがとうございます!!
またAndroid Studioの設定例もありがとうございます🙌
後ほど記事更新してリンクさせていただきます😊

Kurogoma4DKurogoma4D

久々にアイコン差し替えの対応をすることがあって、忘れてしまっていた部分が多かったので参考にさせていただきました 🙏

Androidのアイコン差し替えですが、設定でビルド時に src/main/ressrc/${flavor}/res をマージすることができるので、それを使ったほうが綺麗かなと思います。
https://developer.android.com/studio/build/build-variants?hl=ja#configure-sourcesets

  sourceSets {
    main {
      res.srcDirs = ['src/main/res', 'src/${dartEnvironmentVariables.FLAVOR}/res']
    }
  }
村松龍之介村松龍之介

ありがとうございます!!Androidの知見少ないのでありがたいです🙌
後ほど使わせていただくと思います!

kyam_28kyam_28

参考になる記事をありがとうございます!
launch.json の参考例ですが、余計な { が4行目に含まれているようなので一応記載しておきます!

tomotomo

記事とても参考になりました。ありがとうございます。

macOSでしか動作確認をしておりませんが、 Select GoogleService-Info.plistCompile Sources よりも上にしないと環境が適切に切り替わらなかったように見受けられました。もしどなたかのご参考になりましたら。

Kirk / Kazuki TataiKirk / Kazuki Tatai

大変参考になりました。ありがとうございます🙌

CFBundleDisplayName がiOS端末のホームアイコンに表示されるアプリ名となります。
CFBundleName の方はiOSに限れば使用されている箇所を見つけることができませんでした。

https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundlename

CFBundleName とは、と自身も改めて確認したところ

This name can contain up to 15 characters. The system may display it to users if CFBundleDisplayName isn't set.

とあったので CFBundleDisplayName があれば不要そうでした。自分は CFBundleDisplayName をセットしてCFBundleName を削除してみたところ、正常にビルドされました🙋‍♂️

村松龍之介村松龍之介

Kirkさん、ありがとうございます🙌
CFBundleName を削除しても問題なくビルド可能なんですね👏✨

もしかしたら環境によるかもしれませんが、 CFBundleName は出力した ipa ファイルの名前になるようです👀
CFBundleNamesample の場合は sample.ipa になる)

しかし、このとき CFBundleName に日本語が含まれていた場合省略されるようです。
CFBundleNameサンプルapp の場合、「サンプル」部分が省略されて app.ipa になる)

なので、 CFBundleDisplayName が日本語のアプリなどは分けて定義するメリットがあるかもしれませんね✍️

Kirk / Kazuki TataiKirk / Kazuki Tatai

わざわざコメントありがとうございます!
実はコメントしたあとに「Archive はどうなるんだ…」と気になってました。笑
さらに逆に勉強になりました。ありがとうございます🙏

よねよね

いつもこの記事のお世話になっております🙇

iosのGoogleService-info.jsonの位置がRnner/Runner配下でないと動かない仕様?になっている気がしましたがどうでしょうか?

\cp -f ${SRCROOT}/${FLAVOR}/GoogleService-Info.plist ${SRCROOT}/Runner/GoogleService-Info.plist

また、大変分かりやすいので、Firebase CLI版(androidは変わらず、iosのfile名が変わっただけかもですが...)も執筆していただけることを心待ちにしております!

JboyHashimotoJboyHashimoto

龍之介さん!

すみません、こちらのコマンドを打つ前にbuild.gradlededで、minSdkVersionに数値を入力しないとエラーが出てしまうようです!
最近は、Flutter SDK本体の設定をいじれば、数値を設定できていたのですが、flutter_launcher_iconsは違うみたいです!
驚きました!

flutter pub run flutter_launcher_icons:main

エラーログ

mcz9mmmcz9mm

非常に参考になり助かりました🙏
要ないかもしれませんが、dart-defineを外してビルドした際にデフォルトでdevのアイコンをセットできるようにもしてみたので共有です。

// android/app/build.gradle
def environment = dartEnvironmentVariables.FLAVOR
if (environment == null) {
    environment = "dev"
}
task copySources(type: Copy) {
    from "src/$environment/res"
    into 'src/main/res'
}
ios/scripts/retrieve_dart_defines.sh

~~~~~~~
        echo "#include \"$value.xcconfig\"" >> $OUTPUT_FILE
    fi
done
# 追記 ファイルが空の際にdevを指定する
if [ ! -s $OUTPUT_FILE  ] ; then
  echo "#include \"dev.xcconfig\"" >> $OUTPUT_FILE
fi
村松龍之介村松龍之介

とても遅くなってしまいましたが、 build.gradle の記法など勉強になります!
共有ありがとうございます🙌

いるかいるか

こちらの記事のおかげでアプリ名の出しわけができました!
とても役に立つ記事、ありがとうございます。

marrbormarrbor

前回記事からお世話になっています。ありがとうございます。
Select GoogleService-Info.plistのスクショで記入されているコピーコマンド、

cp -f ${SRCROOT}/${FLAVOR}/GoogleService-Info.plist ${SRCROOT}/GoogleService-Info.plist

cp -f ${SRCROOT}/${flavor}/GoogleService-Info.plist ${SRCROOT}/GoogleService-Info.plist
ですね(実際作業してたら間違えないと思いますが)。

また、Web (Firebase Hosting 使用)ですが、以下でちゃんと環境分けできてます。

> flutter build web --dart-define-from-file=/path/to/json
> firebase deploy --project プロジェクトID --only hosting
村松龍之介村松龍之介

遅くなりましたが、補足コメントありがとうございます🙌
flavor の大文字・小文字のミスですね。あとで修正いたします🙏

kenmakenma

役に立ちました。
extentionを使っていると、ビルド時に次のエラーで失敗します。

error: Embedded binary's bundle identifier is not prefixed with the parent app's bundle identifier.

この記事の方法とコメントで対応できました。

村松龍之介村松龍之介

遅くなりましたが、extensionを使用した場合の補足とコメントありがとうございます🙌

utamoriutamori

この記事の内容ですが、Flutter 3.17で動作しなくなる可能性が高そうです 😱
https://github.com/flutter/flutter/issues/136444#issuecomment-1819780732

nobu.nobu.

前回の記事からお世話になっています。ありがとうございます。

dart_defines/dev.env の googleReversedClientId の行の最後に「,」が入っていますが、「,」があると「"」と「,」が残ったまま環境変数が設定されてしまうようです。

googleReversedClientId の出力例

flutter: "com.googleusercontent.apps.0123456789-xxxxxxxxxxxxxxxx",

dart_defines のファイルは「,」を削除した以下の形が正しいかと思います。

flavor="dev"
appName="FAT dev"
appId="jp.co.altive.fat.dev"
googleReversedClientId="com.googleusercontent.apps.0123456789-xxxxxxxxxxxxxxxx"

google認証をしようとした際に、アプリがクラッシュされる方がいらっしゃったらこれが原因かもしれません。

村松龍之介村松龍之介

ご指摘ありがとうございます!JSONファイルの時の末尾カンマが残ってしまっていたみたいです…!
修正します🙌

RyotaRyota

この構成でプロジェクトを作った場合って、apiKeyなどのセキュアなものってどこで持たせるのがいいのでしょうか。そこだけ綺麗にできずに困ってます。

村松龍之介村松龍之介

秘密鍵などのセキュアなものはアプリに入れてしまうと、成果物から解読されるリスクが高い認識です。

なので、原則サーバーサイドで管理するのが良いかと思っています。
それがどうしても難しい場合は envied等を使って難読化するのが妥協案になりそうです。

RyotaRyota

返信ありがとうございます。
他の方の記事とかで.envにapiKeyを入れてる方がちらほらいたので気になりました。
privateのリポジトリにあげるとしてもアプリ自体を解析されてしまう可能性があるんですね。その辺の知識がなかったので勉強になりました。

K9i a.k.a. たこさんK9i a.k.a. たこさん

build.gradleを編集してdart-defineを受け取るの部分でちょっとハマりそうな挙動を発見したので報告です。

こんな感じにvalueが空の変数を定義したときに

dart_defines/dev.env
hoge=""

pair.first()とpair.last()が両方hogeとなり、keyもvalueもhogeのmapになるみたいです。

android/app/build.gradle
// dart-define を入れる変数を宣言しています。
def dartDefines = [:];
if (project.hasProperty('dart-defines')) {
    // カンマ区切りかつBase64でエンコードされている dart-defines をデコードして変数に格納します。
    dartDefines = dartDefines + project.property('dart-defines')
        .split(',')
        .collectEntries { entry ->
            def pair = new String(entry.decodeBase64(), 'UTF-8').split('=')
            [(pair.first()): pair.last()]
        }
}

対策としてはlengthが2でない場合に空のmapにするなどが考えられそうです。

android/app/build.gradle
- [(pair.first()): pair.last()]
+ pair.length == 2 ? [(pair.first()): pair.last()] : [:]

以下は実際にこのケースに遭遇した際に対応したIssueです。
https://github.com/yumemi-inc/flutter-mobile-project-template/pull/233

村松龍之介村松龍之介

報告ありがとうございます🙌
なるほど!空文字の値を設定してしまうと、lastpair もKeyの方を参照してしまうんですね…!
勉強になりました✍️
後ほど対策取り入れさせていただくと思います🙏

やまやま

記事の執筆ありがとうございます🙇‍♂️

当方のFlutter 3.29.0 の環境でAndroid版のビルドをしようとしたら問題が発生し、少し修正を加えました。

Androidデバッグビルドがうまく行かず…

記事の通りにAndroid版の設定を更新し、dart-define-from-fileを使ったアプリのデバッグを試みたところ、エラーが発生しました。

エラー詳細
Launching lib/main.dart on sdk gphone64 arm64 in debug mode...

FAILURE: Build failed with an exception.

* What went wrong:
A problem was found with the configuration of task ':app:selectGoogleServicesJson' (type 'Copy').
  - Gradle detected a problem with the following location: '<PATH_TO_PROJ>/android/app'.

    Reason: Task ':app:compileFlutterBuildDebug' uses this output of task ':app:selectGoogleServicesJson' without declaring an explicit or implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed.

    Possible solutions:
      1. Declare task ':app:selectGoogleServicesJson' as an input of ':app:compileFlutterBuildDebug'.
      2. Declare an explicit dependency on ':app:selectGoogleServicesJson' from ':app:compileFlutterBuildDebug' using Task#dependsOn.
      3. Declare an explicit dependency on ':app:selectGoogleServicesJson' from ':app:compileFlutterBuildDebug' using Task#mustRunAfter.

    For more information, please refer to https://docs.gradle.org/8.2/userguide/validation_problems.html#implicit_dependency in the Gradle documentation.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 2s
Error: Gradle task assembleDebug failed with exit code 1

Exited (1).

エラーではタスクの依存関係の設定が良くないと言われたので、app/build.gradle のif文に新たに条件を追加してみた所、Debugモードのビルドはうまくいきました。

tasks.whenTaskAdded { task ->
    if (task.name == 'processDebugGoogleServices' || 
        task.name == 'processReleaseGoogleServices' ||
        // 以下自分で追加したもの
        task.name == 'compileFlutterBuildDebug' ||
        task.name == 'compileFlutterBuildProfile' ||
        task.name == 'compileFlutterBuildRelease' ||
        task.name == 'mergeDebugShaders' ||
        task.name == 'mergeProfileShaders' ||
        task.name == 'mergeReleaseShaders' ||
        task.name == 'mergeDebugJniLibFolders' ||
        task.name == 'mergeProfileJniLibFolders' ||
        task.name == 'mergeReleaseJniLibFolders'
    ) {
        task.dependsOn selectGoogleServicesJson
    }
}

リリースビルドができず

上記対応により、デバッグのビルドは成功するようになりましたが、リリースビルドには失敗してしまいました。

エラー詳細
% flutter build appbundle --dart-define-from-file=dart_defines/prod.env 

Running Gradle task 'bundleRelease'...                          
Warning: SDK processing. This version only understands SDK XML versions up to 3 but an SDK XML file of version 4 was encountered. This can happen if you use versions of Android Studio and the command-line tools that were released at different times.
ERROR: /<PATH_TO_DIR>/build/app/intermediates/merged_java_res/release/base.jar: R8: com.android.tools.r8.ResourceException: com.android.tools.r8.internal.vc: I/O exception while reading '/<PATH_TO_DIR>/build/app/intermediates/merged_java_res/release/base.jar': /<PATH_TO_DIR>/build/app/intermediates/merged_java_res/release/base.jar

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:minifyReleaseWithR8'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.R8Task$R8Runnable
   > Compilation failed to complete, origin: /<PATH_TO_DIR>/build/app/intermediates/merged_java_res/release/base.jar

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 22s
Running Gradle task 'bundleRelease'...                             22.9s
Gradle task bundleRelease failed with exit code 1

そこで :app:minifyReleaseWithR8 というタスクに関連していろいろ調べてみた所、新しいAGP (私の環境では8.2.1です) では tasks.whenTaskAddedを使っている部分は tasks.configureEachに置き換える必要があるらしいことがわかりました。

https://manacus.aurantiacus.f5.si/daja/2023/09/000023.html#:~:text=上記のエラーは app/build.gradle の中で「tasks.whenTaskAdded { … }」を使用していると発生するようで、これを代替の「tasks.configureEach { … }」に置き換えたところ、手元ではエラーが解消しました。
https://qiita.com/kilalabu/items/5289fd216c71e1f2d3ed#:~:text=whenTaskAddedを使っている箇所があれば、configureEachに置き換えよとのことでした。

そこで、whenTaskAddedconfigureEachに置き換えた所、問題なくリリースビルドが成功しました。

まとめ

最新のFlutter/AGP環境では tasks.whenTaskAddedtasks.configureEachに置き換えていくつか条件文を追加しましょう。