【Flutter】Firebase(Firestore) 疎通までのチュートリアル

# 概要
Flutter でFirebase を利用してみようと、webの情報を参考に進めていましたが、古い情報も多いらしく上手くいかないことがありました。
結局、[公式ドキュメント](https://firebase.google.com/docs/flutter/setup?hl=ja&platform=android)を参考に進めましたが、それでも躓いた点があったため、メモとして残しておきます。

Flutter 学習中の初心者の記事のため、誤った情報が含まれているかもしれません。
間違っている情報や、改善点あればコメントにて教えていただけると嬉しいです。

なお、記事中の情報は、2021/07/04 現在のものです。

# ターゲット
- Flutter + Firebase の組合せ、チュートリアル通りに進めているのに、何故かうまくいかない人

# この記事の説明範囲
- Firebase プロジェクトの作成からFirebase(Firestore)への疎通確認まで
    - アプリで入力したデータをFirestore で保存するアプリ作成
    - 保存するだけで読み出し部分は作らない

# 準備
## 0. [AndroidStudio] テスト用プロジェクトを作成
Flutter プロジェクトを作成
名前は何でもいいですが、今回は"firebase-test"とします。

## 1.[Firebase] プロジェクトを作成する
[Firebase console](https://console.firebase.google.com/) より、プロジェクトを作成します。
こちらも名前は何でもいいですが、ここでは先ほどの名前と揃えておきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188220/a5ffedd2-4c35-5ac7-a2a1-af3abc98a4f3.png" width="600" height="300">

その後の選択はデフォルトのまま進めてOKです
## 2.アプリの登録
作成したプロジェクト画面より、android を選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188220/d42f6c29-b0fb-05ba-8830-8198ba3a72e5.png" width="600" height="300">
### 2.1 アプリ情報を入力
- Android パッケージ名
    - ``android\app\build.gradle`` のapplicationId に記載されているもの
    - 他は空欄のままで「アプリを登録」
   
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188220/645eacaf-2d3d-e1d3-d3dc-7ecc564c2c62.png" width="400">



       
## 3.google-services.json をダウンロード
画面に従い``google-services.json`` をダウンロード。
ダウンロードしたgoogle-services.json を Flutterプロジェクトの``(project)\android\app``直下に配置。
![2021-07-04_11h15_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188220/137a4dc9-82a8-c802-65b7-2479ddb394c7.png)
## 4.build.gradle の修正
``android\build.gradle`` 、``android\app\build.gradle`` 2つのgradle に下記の通り追記する
<font color="red">(同名ファイルが２つあるので注意）</font>

### android\build.gradle
```gradle:android\build.gradle
buildscript {
  repositories {
    // 下記行があることを確認（なければ追加しておく）
    google()  // Google's Maven repository
  }
  dependencies {
    ...
    // 下記行を追加
    classpath 'com.google.gms:google-services:4.3.8'
  }
}
allprojects {
  ...
  repositories {
    // 下記行があることを確認（なければ追加しておく）
    google()  // Google's Maven repository
    ...
  }
}
```

### android\app\build.gradle

```gradle:android\app\build.gradle
// "android" セグメントの上に行を追加
apply plugin: 'com.google.gms.google-services'  // Google Services plugin

android {
  // ...
}
```

## 5.CloudFirestoreプラグインのインストール
[(公式)Firestoreドキュメント](https://firebase.flutter.dev/docs/firestore/overview/)
１. ``pubspec.yaml`` を編集

```yaml:android\pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  firebase_core: "^1.3.0" # 追加
  cloud_firestore: "^2.2.2" # 追加
```
２. AndroidStudioのターミナルより ``flutter pub get`` 実行
３. ``flutter run`` を実行し、正常に起動することを確認しておく
<font color="red">※嵌りポイント</font>
ここで下記のエラーが発生することがあります

```
D8: Cannot fit requested classes in a single dex file (# methods: 79185 > 65536)
com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: 
The number of method references in a .dex file cannot exceed 64K.
Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html
...
(以下略)
```

発生した場合は、[エラーメッセージの解決方法](https://developer.android.com/tools/building/multidex.html) に従い、下記の通り``android\app\build.gradle`` を編集後、再度``flutter run`` してみてください。

```gradle:android\app\build.gradle
android {
    defaultConfig {
        ...
        minSdkVersion 15
        targetSdkVersion 28
        multiDexEnabled true // 追加
    }
    ...
}
dependencies {
    implementation "androidx.multidex:multidex:2.0.1" // 追加
}
```
## 6.CloudFirestore データベースの作成
１. Firebase のコンソールから"Firestore Database"の「データベースの作成」を選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188220/eb92d972-9408-1a61-c8af-3ffcde5b8637.png" width="450">
２．「テストモードで開始する」にチェックを入れ次へ
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188220/337b2cec-cec8-5a15-041d-c70855820748.png" width="450">
３．あとはデフォルトのままでOK


これで準備は完了です。
この後は実際にFlutterプロジェクトからFirestore を利用してみます。

# Firebase を利用してみる
[(公式)Firestoreチュートリアル](https://firebase.flutter.dev/docs/firestore/usage) を参考に、簡単なサンプルプログラムを作成しました。
ソースコードは後述するので、必要に応じてコピペしてください。
氏名、年齢を入力し、「ユーザ登録」ボタンを押下するとFirestore にデータが保存されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188220/f5bdeffb-c344-4e7e-2454-39e83b162285.png" width="250" height="300">

登録ボタン押すと、Firestore上にデータが保存される。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188220/98f12bb4-0392-8c7b-4858-909866c0b043.png" width="950" >


## ソースコード
### main.dart
<details><summary>長いので折り畳み</summary><div>

``` dart:main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_test/add_user.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized(); // 追加
  await Firebase.initializeApp(); // 追加
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _name = "";
  int _age = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Flutter + Firebase デモ"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              decoration: InputDecoration(
                labelText: "氏名",
                hintText: "田中 太郎",
              ),
              onChanged: (inputText) {
                setState(() {
                  _name = inputText;
                });
              },
            ),
            TextField(
              keyboardType: TextInputType.number,
              inputFormatters: [FilteringTextInputFormatter.digitsOnly],
              decoration: InputDecoration(
                labelText: "年齢",
                hintText: "28",
              ),
              onChanged: (inputText) {
                print(inputText);
                setState(() {
                  _age = int.parse(inputText);
                });
              },
            ),
            AddUser(_name, _age),
          ],
        ),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
```
</div></details>

### add_user.dart(新規作成)
libフォルダ配下に``add_user.dart``を新規作成で追加
<details><summary>長いので折り畳み</summary><div>

```dart:add_user.dart
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class AddUser extends StatelessWidget {
  final String name;
  final int age;

  AddUser(this.name, this.age);

  @override
  Widget build(BuildContext context) {
    CollectionReference users = FirebaseFirestore.instance.collection('users');
    Future<void> addUser() {
      print(this.age);
      return users
          .add({
        'full_name': name, // 田中太郎
        'age': age // 42
      })
          .then((value) => print("User Added"))
          .catchError((error) => print("Failed to add user: $error"));
    }
    return ElevatedButton(
      onPressed: addUser,
      child: Text(
        "ユーザ登録",
      ),
    );
  }
}
```
</div></details>
## 実装上の注意点
<font color="red">※嵌りポイント</font>
Firebase の使用には、main関数内で下記の記述が必要？っぽい。
少なくともFirestore のドキュメントには記載なさそうだったけどどこかに記載あるのかな。

```dart:main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized(); // 追加
  await Firebase.initializeApp(); // 追加
  runApp(MyApp());
}
```


# 参考リンク
- [(公式チュートリアル)Flutter アプリに Firebase を追加する](https://firebase.google.com/docs/flutter/setup?hl=ja&platform=android)
- [(公式チュートリアル)Cloud Firestore](https://firebase.flutter.dev/docs/firestore/usage/)
- [64KBを超えるメソッドを使用するアプリ向けにmultindexを有効化する](https://developer.android.com/studio/build/multidex)