Flutter Web で Firebase JavaScript SDK が使えるのか試してみた

## 概要

こちらは [Flutter #2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/flutter-2) の 14日目の記事です。

まだ、本格的に開発で使っていないのですが、iOS/Androidのアプリ開発に加えてゆくゆくWebアプリも開発できる予定のマルチプラットフォーム [Flutter](https://flutter.dev/) をキャッチアップ中で調査したり触ったりしております。

先日、12/2に開催された [PORT Firebase × Flutter オンライン](https://stamp.connpass.com/event/194519/) に参加しました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/PORTJP?src=hash&amp;ref_src=twsrc%5Etfw">#PORTJP</a> <br>React/Vueはhot reloadがあるのにFlutter Webはhot reloadが無いんですね汗<br><br>ありがとうございます！！！</p>&mdash; gremito #ものラジ QAスクラムでCSM® フリーランス (@grem_ito) <a href="https://twitter.com/grem_ito/status/1334099645211938817?ref_src=twsrc%5Etfw">December 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

そこで気になったことが、iOS/Androidのビジネスロジック（ `/lib` 配下のDartクラス ）とは分離してしまうことになるものの、Flutter Web に [Firebase JavaScript SDK](https://firebase.google.com/docs/web/setup) が組み込めて使えるのか調査してみることにしました。


## 前提

Flutter と Firebase の入門的な話（前提知識やスタートガイドなどの情報）は割愛いたしますmm
公式のドキュメントがしっかり解説されており、参考になる記事も増えてきているため、本記事以外を参考によろしくお願いします。

また Flutter Web は、公式が正式版をまだ公開しておらず、ベータ版で提供されています。
そのため、本記事は責任を一切負えないため、参考程度に使用してください。


## Flutter と Firebase をどう管理するか

まずは、[Building a web application with Flutter](https://flutter.dev/docs/get-started/web) の内容を済ませておき、`flutter create`でプロジェクト用意しておく。

その後に`firebase init`しますが、同じ階層でFirebaseも一緒に管理するとなると困難になり、挙動が変わる原因にもなりかねないため以下のようにディレクトリを分けてFlutterリソースとFirebaseリソースを管理すると良いでしょう。

　<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/a194b477-4f62-debf-3715-94047fa60e5e.png">


## Firebase JavaScript SDK

　<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/22491ec7-9bbd-dc59-0ab7-360fee2255df.png">

Firebaseコンソールのダッシュボードに表示されている↑Webアプリの追加を済ませて、設定画面からJavascript SDKの組み込み方法を確認する。

　<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/5b68f2ed-6ea1-af51-f913-75cc7c2e24d5.png">　<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/0805fce6-3e34-5e39-ce5c-c32da8c28f81.png">

このまま [Firebase Hosting](https://firebase.google.com/docs/hosting) で Flutter Web リソースをアップして対応まで考えると左の図で良いのですが、そもそも使えるのかわからないので、まずは右のCDN版で実行して使えることを確かめていきます。

### Firebase SDK snippet

上記右画像の`Firebase SDK snippet`に書かれている組み込み方法をそのまま書いて実行してみたところ次のようにエラーで正常動作しませんでした。

```html:flutter/web/index.html
<!DOCTYPE html>
<html>
<head>
  <base href="/">

  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="A new Flutter project.">

  <!-- iOS meta tags & icons -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="flutter_web_sample">
  <link rel="apple-touch-icon" href="icons/Icon-192.png">

  <!-- Favicon -->
  <link rel="icon" type="image/png" href="favicon.png"/>

  <title>flutter_web_sample</title>
  <link rel="manifest" href="manifest.json">
</head>
<body>
  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('flutter-first-frame', function () {
        navigator.serviceWorker.register('flutter_service_worker.js');
      });
    }
  </script>
  <script src="main.dart.js" type="application/javascript"></script>

  <!-- The core Firebase JS SDK is always required and must be listed first -->
  <script src="https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js"></script>

  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      authDomain: "flutter-server-app.firebaseapp.com",
      projectId: "flutter-server-app",
      storageBucket: "flutter-server-app.appspot.com",
      messagingSenderId: "01234567890",
      appId: "0:01234567890987654321:web:abcd01234efghi56789"
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
  </script>
</body>
</html>
```

 ![スクリーンショット 2020-12-14 18.29.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/7d02bbd3-cca6-35bc-28bb-608ddb99a6a3.png)

```sh
$ flutter run -d chrome
Running "flutter pub get" in flutter...                          1,117ms
Launching lib/main.dart on Chrome in debug mode...
Syncing files to device Chrome...                                  37.3s
Debug service listening on ws://127.0.0.1:58489/XXXXXXXXXXXX=/ws

Warning: Flutter's support for web development is not stable yet and hasn't
been thoroughly tested in production environments.
For more information see https://flutter.dev/web

🔥  To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following JSNoSuchMethodError was thrown building MyApp(dirty):
TypeError: Cannot read property 'MaterialApp' of undefined

The relevant error-causing widget was:
  MyApp file:///Users/TAKUYA_AIR/flutter-projects/flutter_web_sample/flutter/lib/main.dart:4:10

When the exception was thrown, this was the stack:
packages/flutter_web_sample/main.dart 11:12                              build
packages/flutter/src/widgets/framework.dart 4749:28                      build
packages/flutter/src/widgets/framework.dart 4675:15                      performRebuild
packages/flutter/src/widgets/framework.dart 4369:5                       rebuild
packages/flutter/src/widgets/framework.dart 4654:5                       [_firstBuild]
packages/flutter/src/widgets/framework.dart 4649:5                       mount
packages/flutter/src/widgets/framework.dart 3615:13                      inflateWidget
packages/flutter/src/widgets/framework.dart 3380:18                      updateChild
packages/flutter/src/widgets/binding.dart 1203:16                        [_rebuild]
packages/flutter/src/widgets/binding.dart 1174:5                         mount
packages/flutter/src/widgets/binding.dart 1116:16                        <fn>
packages/flutter/src/widgets/framework.dart 2730:19                      buildScope
packages/flutter/src/widgets/binding.dart 1115:12                        attachToRenderTree
packages/flutter/src/widgets/binding.dart 954:24                         attachRootWidget
packages/flutter/src/widgets/binding.dart 936:7                          <fn>
dart-sdk/lib/_internal/js_dev_runtime/private/isolate_helper.dart 48:19  internalCallback

════════════════════════════════════════════════════════════════════════════════════════════════════
```

dartのエラーは、おそらくhtmlサイドでエラーになっているから連携できず影響しているんだと思って、`flutter run`に出ているdartに関するエラーはあまり気にしないことにしました。

### 公式ドキュメント

次に JavaScript SDKのスタートガイドページの [ステップ 3: Firebase SDK を追加して Firebase を初期化する](https://firebase.google.com/docs/web/setup?hl=ja#add-sdks-initialize) を確認して以下の箇所を変えてみました。すると `flutter run` にエラーは表示されず、やはり正常動作ではないものの結果が変わりました。

```html:flutter/web/index.html
【省略】

  <!-- The core Firebase JS SDK is always required and must be listed first -->
  <!-- <script src="https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js"></script> -->
  <script defer src="https://www.gstatic.com/firebasejs/7.23.0/firebase-app.js"></script>
  <script defer src="https://www.gstatic.com/firebasejs/7.23.0/firebase-auth.js"></script>
  <script defer src="https://www.gstatic.com/firebasejs/7.23.0/firebase-firestore.js"></script>

【省略】
```

　<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/50de3a01-fcd5-40eb-e381-50cf8f28a6dc.png">


### firebase_core_web

調べていると [[firebase_core_web] Uncaught ReferenceError: firebase is not defined #2826](https://github.com/FirebaseExtended/flutterfire/issues/2826) というissueを見つけて、内容を確認すると本記事と同じく Flutter Web で Firebase を組み込んだ際の問題でした。

ただ、Closeされているものの何が原因だったのかわからずで、とりあえず思ったことを試していくことに。
以前、Flutter のセットアップの際に[iOSだけエラーだったもののWebだけ開発するなら大丈夫だろうと無視していたが、結局 `flutter doctor` が全て正常でなければビルドできない](https://github.com/flutter/flutter/issues/71535)経験がありました。
もしかすると今回も似たような感じで、Webだけ対応していてiOS/AndroidはFirebaseの対応していないからエラーになっている可能性があるなと思い、`firebase_core_web`を入れて確認してみるものの変化はありませんでした・・・笑汗

[firebase_core_web/install](https://pub.dev/packages/firebase_core_web/install)
 

## Web Installation | FlutterFire

`firebase_core_web` をしっかり確認していくと、ここで Flutter と Firebase を連携する際に提供しているドキュメントがあることを改めて確認（存在は知っていました笑）し、JavaScript SDK の組み込み方法が解説されている [Web Installation](https://firebase.flutter.dev/docs/installation/web/) の存在がわかりましたw

どうやら`flutter_service_worker.js`や`main.dart.js`の準備よりも前に Firebase を組み込むようでした。
案の定、正常動作したので試しに [Firebase RemoteConfig](https://firebase.google.com/docs/remote-config/use-config-web?hl=ja) と連携してみました。


```html:flutter/web/index.html
【省略】

  <!-- The core Firebase JS SDK is always required and must be listed first -->
  <!-- <script src="https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js"></script> -->
  <!-- <script defer src="https://www.gstatic.com/firebasejs/7.23.0/firebase-app.js"></script>
  <script defer src="https://www.gstatic.com/firebasejs/7.23.0/firebase-auth.js"></script>
  <script defer src="https://www.gstatic.com/firebasejs/7.23.0/firebase-firestore.js"></script> -->
  <script src="https://www.gstatic.com/firebasejs/7.23.0/firebase-app.js"></script>

  <script>
    // Your web app's Firebase configuration
    var firebaseConfig = {
      apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      authDomain: "flutter-server-app.firebaseapp.com",
      projectId: "flutter-server-app",
      storageBucket: "flutter-server-app.appspot.com",
      messagingSenderId: "01234567890",
      appId: "0:01234567890987654321:web:abcd01234efghi56789"
    };
    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);

    // Remote Config
    const remoteConfig = firebase.remoteConfig();
    remoteConfig.fetchAndActivate()
      .then(() => {
        const serviceName = remoteConfig.getValue('service_name');
        console.log("service_name: " + serviceName._value);
      })
      .catch((err) => {
        console.error(err);
      });
  </script>

  <script>
    if ('serviceWorker' in navigator) {
      window.addEventListener('flutter-first-frame', function () {
        navigator.serviceWorker.register('flutter_service_worker.js');
      });
    }
  </script>

  <script src="main.dart.js" type="application/javascript"></script>

【省略】
```

　<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/62763dfa-1ab4-607b-6fa3-1e77eca5a269.png">
　<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/6edd9720-ae7b-0a24-e43e-9e83d495a5d6.png">


## Hosting

JavaScript SDK を調整して Hosting にアップして確認したところ問題なく動きました！

```sh
$ cd flutter
$ flutter build web
Running "flutter pub get" in flutter...                            573ms
Compiling lib/main.dart for the Web...                             29.2s

$ cd ../firebase
$ cp -r ../flutter/build/web/ public/

$ firebase deploy --only hosting

=== Deploying to 'flutter-server-app'...

i  deploying hosting
i  hosting[flutter-server-app]: beginning deploy...
i  hosting[flutter-server-app]: found 14 files in public
✔  hosting[flutter-server-app]: file upload complete
i  hosting[flutter-server-app]: finalizing version...
✔  hosting[flutter-server-app]: version finalized
i  hosting[flutter-server-app]: releasing new version...
✔  hosting[flutter-server-app]: release complete

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/flutter-server-app/overview
Hosting URL: https://flutter-server-app.web.app
```

![スクリーンショット 2020-12-14 19.59.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82601/247c0ee9-8ff4-fdc6-28c1-fa99a46a5caf.png)


## まとめ

Flutter に Firebase を導入される際は、 [FlutterFire](https://firebase.flutter.dev/) を参考にしましょうー！😇