[Flutter × Firebase] Webで動作したコードをiOSでも動かそうとした際にハマったこと

2022.09.30

こんにちは、CX事業本部 IoT事業部の若槻です。

Firebase Analyticsを導入したFlutterアプリのデバッグをしていた際に、同じソースコードでWebだと動くのにiOSだと上手く動かないという事象に遭遇したので対処した際のメモです。

前提

使用したソースコードは以下のエントリで作成したものです。

ソースコードは次のようになります。flutter createで作成されたmain.dartでFirebase Analyticsを使用したログ送信がされるようにしています。

lib/main.dart

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'firebase_options.dart';

void main() async {
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  await FirebaseAnalytics.instance.logEvent(
    name: 'MyApp',
  );

  runApp(const MyApp());
}

//以下略

上記コードで、flutter run -d chromeコマンドでWebアプリで起動した際は問題なく実行できていました。

事象

その1

アプリをiOSで起動しようとすると、DefaultFirebaseOptions have not been configured for iosというメッセージのエラーが出て起動できませんでした。

$ flutter run
Launching lib/main.dart on iPhone 14 in debug mode...
Running Xcode build...                                                  
 └─Compiling, linking and signing...                      2,513ms
Xcode build done.                                            8.9s
[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: Unsupported operation: DefaultFirebaseOptions have not been configured for ios - you can reconfigure this by running the FlutterFire CLI again.
#0      DefaultFirebaseOptions.currentPlatform (package:flutter_sample_app/firebase_options.dart:29:9)
#1      main (package:flutter_sample_app/main.dart:8:37)
#2      _runMain.<anonymous closure> (dart:ui/hooks.dart:134:23)
#3      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#4      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
Syncing files to device iPhone 14...                                77ms

Flutter run key commands.
r Hot reload. 
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

 Running with sound null safety 

An Observatory debugger and profiler on iPhone 14 is available at: http://127.0.0.1:50278/mz6VS6iSbzc=/

Firebase CLIの再ログインや、FlutterのconfigでiOSを有効化してみましたが、解消しません。

# Firebase CLIの再ログイン
$ firebase logout & firebase login

# FlutterのconfigでiOSを有効化
$ flutter config --enable-ios
Setting "enable-ios" value to "true".

You may need to restart any open editors for them to read new settings.

解決

設定すべきはFirebase CLIではなく、FlutterFire CLIでした。(エラーメッセージにもちゃんとそう書いてありますね。)

ドキュメントに従ってFlutterFireの導入と設定をします。

# 導入
$ dart pub global activate flutterfire_cli
$ export PATH="$PATH":"$HOME/.pub-cache/bin"

# 設定
$ flutterfire configure
 Which platforms should your configuration support (use arrow keys & space to select)? · ios, web

その2

その1の対応後に、改めてflutter runを実行すると、次はBinding has not yet been initialized.という別のエラーが発生するようになりました。

$ flutter run
Launching lib/main.dart on iPhone 14 in debug mode...
Running pod install...                                           1,443ms
Running Xcode build...                                                  
 └─Compiling, linking and signing...                      2,523ms
Xcode build done.                                           10.9s
[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: Binding has not yet been initialized.
The "instance" getter on the ServicesBinding binding mixin is only available once that binding has been initialized.
Typically, this is done by calling "WidgetsFlutterBinding.ensureInitialized()" or "runApp()" (the latter calls the former). Typically this call is done in the "void main()" method. The "ensureInitialized" method is idempotent; calling it multiple times is not harmful. After calling that method, the "instance" getter will return the binding.
In a test, one can call "TestWidgetsFlutterBinding.ensureInitialized()" as the first line in the test's "main()" method to initialize the binding.
If ServicesBinding is a custom binding mixin, there must also be a custom binding class, like WidgetsFlutterBinding, but that mixes in the selected binding, and that is the class that must be constructed before using the "instance" getter.
#0      BindingBase.checkInstance.<anonymous closure> (package:flutter<…>
Syncing files to device iPhone 14...                                55ms

Flutter run key commands.
r Hot reload. 
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

 Running with sound null safety

解決

こちらの記事が参考になりました。

main()の冒頭にWidgetsFlutterBinding.ensureInitialized();を追記します。

lib/main.dart

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

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  await FirebaseAnalytics.instance.logEvent(
    name: 'screen_view',
    parameters: {
      "screen_name": "my_app_screen",
      "content_type": "image",
      "item_id": "5678",
    },
  );
  runApp(const MyApp());
}

ensureInitialized()を使用すると、runApp()が呼び出される前にbindingのinitializeを必ず行うようにできるようです。iOSアプリの場合だとFirebase.initializeAppの実行が完了する前にrunApp()が呼び出されていたためエラーになっていたという所でしょうか。

Returns an instance of the binding that implements [WidgetsBinding]. If no binding has yet been initialized, the [WidgetsFlutterBinding] class is used to create and initialize one.

You only need to call this method if you need the binding to be initialized before calling [runApp].

In the flutter_test framework, [testWidgets] initializes the binding instance to a [TestWidgetsFlutterBinding], not a [WidgetsFlutterBinding]. See [TestWidgetsFlutterBinding.ensureInitialized].

iOS Simulatorでアプリの起動を試してみると、正常に起動できるようになりました。

$ flutter run          
Launching lib/main.dart on iPhone 14 in debug mode...
Running Xcode build...                                                  
 └─Compiling, linking and signing...                         4.3s
Xcode build done.                                           13.0s
Syncing files to device iPhone 14...                               139ms

Flutter run key commands.
r Hot reload. 
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

 Running with sound null safety 

An Observatory debugger and profiler on iPhone 14 is available at: http://127.0.0.1:58122/dU2EC-G_PHk=/
The Flutter DevTools debugger and profiler on iPhone 14 is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:58122/dU2EC-G_PHk=/

以上