FlutterからFirestoreに接続する時にNo Firebase App '[DEFAULT]' has been created

先日Flutter(for web)Firebase内のデータを読み書きするコードを書こうとしていたときに本当に本当に困ったので、誰かの助けになればと思い残しておきます。記事やQA系サイトを見ていると未解決のケースも多かったように思います。

1年ほど前にFlutter webを触ったときにはまだBetaですが、無事stableになったんですね。めでたしめでたし。SDKもかなり進化して使いやすくなっていました。ただ、製品の更新の速度にドキュメント類やネット上の記事が追いついておらず古いコードが散在しています。ちなみに、過去に動いていたアプリも動かなくなっていました。後方互換性はさすがに維持してほしいですね。

**なお、本記事で私の環境での対策は書いているものの、検証結果や推察が正しいことは保証できませんし、結局理解不能に終わったところもありますので悪しからず。**

# 困ったこと
FlutterのアプリからFirebaseにアクセスしようとすると```Error: [core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()```が発生します。そもそもの原因は2020年8月頃のUpdateにより、Flutterアプリ内でFirebaseを利用する前に```Firebase.intializeApp()```を呼び出さなければいけなくなったことです。

この解決法は非常に有名で、例えば以下の記事が手っ取り早いかと思います。
https://qiita.com/mamoru_takami/items/87a20d861806a70db29d

ただ私の場合はこれで改善できませんでした。正確に言うと改善はできているのですが、ネット上の記事やFlutter、Firebaseのドキュメントにミスリードされていました。

# TL;DR
Firebaseで新規のアプリケーションを作成すると以下のようなコードが生成され、```index.html```等のフロントページに貼り付けろと言われますが、結論を言うと私の環境では先頭行のimport文が不要でした。[結論](#結論)も併せて参照ください。

```javascript
import { initializeApp } from "firebase/app";   /* ←これが不要 */
 // Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "XXX",
  authDomain: "XXX",
  projectId: "XXX",
  storageBucket: "XXX",
  messagingSenderId: "XXX",
  appId: "XXX"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
```

なお、そうすると最終行の```initializeApp()```の呼び出しが通らなくなるので、index.html内に以下を追加します。
Firestoreを使うなら2行目も必要です。

```html
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-firestore.js"></script>
```

その上で、先程の最終行の呼び出しは以下のように変更します。

```javascript
//const app = initializeApp(firebaseConfig);
const app = firebase.initializeApp(firebaseConfig);
```

詳細は以下に書きます。

# 環境
* Flutter 2-6-0-11.pre
* channelはdev
* Dart2.15.0

# 理屈
Flutterに限らずFirebaseに接続するあらゆるコードは、実際の読み書きを行う前にinitialize処理を行う必要があります。これは新verの仕様だそうです。しかし、**このinitialize処理は1回だけ**呼び出さなくてはいけません。複数回作るとそのたびに接続のインスタンスが生成されてしまい、詳細は省きますが意図した挙動にならないようです。

私の場合は上記の通りのindex.html、それからDart側のmain.dartに以下のようにコードを書いていました。
これは先程貼ったQiitaの記事の追記部分に記されている方法で、Firebaseへの読み書きが頻繁に生じるようならばmain関数実行時にinitializeをしてしまえ、という理屈なので納得できます。

```dart:main.dart
// 各種import文
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';

void main() async {
  await Firebase.initializeApp();
  runApp(MyApp());
}
```

しかし、どうやらindex.html内のモジュール呼び出しで既にinitalizeが行われている（？）ようです。
これを消したらErrorは発生しなくなりました。

```javascript:index.html
import { initializeApp } from "firebase/app";   /* ←これが不要 */
```

# 実験
いくつか不思議な点が残りましたので検証をしてみました。あくまでも検証なので違うかもしれません。もし知っている方がいらっしゃったら教えて下さい。なお、短期間にサーバの再起動を繰り返していると```initializeApp```されたApplicationが残ってしまっていて正常に検証できない可能性があるため、一部検証ではmain関数内は以下のように調整をして検証をしました。

```dart
void main() async {
  print(Firebase.apps.length);
  if (Firebase.apps.length != 0) {
    Firebase.apps[0].delete();
  }
  await Firebase.initializeApp();
  runApp(WebConsoleTop());
}
```

## 検証1：index.htmlでのimport宣言は残し、main.dartの```initializeApp()```を行わない
これはダメでした。dartコード側でのintializeApp()は必要なようです。Error内容は変わらず、```No Firebase App '[DEFAULT]' has been created```です。ここのエラーメッセージをもうちょっと切り分けてくれたら良いんですけどね。


```javascript:index.html
import { initializeApp } from "firebase/app";   /* ←これが不要 */
 // Your web app's Firebase configuration
const firebaseConfig = {
 //省略
};
// Initialize Firebase
// const app = initializeApp(firebaseConfig); 行わない
```

```dart:main.js
void main() {
  runApp(WebConsoleTop());
}
```

## 検証2：index.htmlのimport宣言、import.htmlの```const app = initializeApp(firebaseConfig)```も行わず、main.dartでのみinitializeする
index.htmlのinitialize処理は本当にimport部分で行われているのでしょうか？常識的に考えたら、どう見ても```initializeApp()```関数内で行われているようにしか見えません。というか、生成されたコメントにそう書いてありますしね。

まあ、これに関してはconfig情報を流し込んでいないのでコケる気はしてまして、案の定だめでした。なお、やはりError内容は```FirebaseError: Firebase: No Firebase App '[DEFAULT]' has been created```です。

```javascript:index.html
 // import { initializeApp } from "firebase/app";   行わない
 // Your web app's Firebase configuration
const firebaseConfig = {
 //省略
};
// Initialize Firebase
// const app = initializeApp(firebaseConfig); 行わない
```

```dart:main.js
void main() async {
  print(Firebase.apps.length);
  if (Firebase.apps.length != 0) {
    Firebase.apps[0].delete();
  }
  await Firebase.initializeApp();
  runApp(WebConsoleTop());
}
```

## 検証3：index.html内の```initializeApp(firebaseConfig)```は残し、main.dartでは```initializeApp()```を行わない
理屈としてはConfig情報もちゃんと入りますので行ける気はします。というか、昔のバージョンのコードと同じですね。
通らないと思っていましたが、通りました。解せません。確かに初回はダメだった記憶があります。サーバサイドで何かしらのtoken情報などを一定時間保持してしまっているのかもしれません。結局、index.html内でのimport宣言を無くすのが無難のように思います。

```javascript:index.html
 // import { initializeApp } from "firebase/app";   行わない
 // Your web app's Firebase configuration
const firebaseConfig = {
 //省略
};
// Initialize Firebase
   const app = initializeApp(firebaseConfig); 行わない
```

```dart:main.dart
void main() {
  runApp(WebConsoleTop());
}
```

# 結論
FirebaseでApplicationを作成したときに生成されるJavaScriptコードはimport文が不要、但し要微調整です。

```html:index.html

<!-- 追加する -->
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/8.6.1/firebase-firestore.js"></script>

<script>
 /*import { initializeApp } from "firebase/app";    ←これが不要 */
 // Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "XXX",
  authDomain: "XXX",
  projectId: "XXX",
  storageBucket: "XXX",
  messagingSenderId: "XXX",
  appId: "XXX"
};
// Initialize Firebase
const app = firebase.initializeApp(firebaseConfig);　//firebase.initializeAppに変更

</script>
```

```dart:main.dart
// 各種import文
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';

void main() async {
  await Firebase.initializeApp();
  runApp(MyApp());
}
```

解決に手間取り、GitHubのissueを見たりソースを読んだりすることで結果的には色々と勉強になったような気がします。
繰り返しになりますがあくまでも個人的な見解ですので、正確な情報があれば教えて下さい。