FlutterでFirebaseを使ってみる〜投票アプリ〜

自分の備忘録用記事です。
※2021/03/03 [こちらのページ](https://firebase.flutter.dev/docs/overview)に従い、firebase_coreを使うパターンも記載しました。

完成形
![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/5773ff0b-60b6-7dd0-09bc-1e45d07fc958.gif)


下記の記事を参考にしてます。

https://codelabs.developers.google.com/codelabs/flutter-firebase/index.html#0

## Flutter projectの作成

まずはFlutter projectの作成。
今回はAndroid Studioメインで作成します。
(Flutterのプラグインは導入済みの想定。まだの人は[こちら](https://flutter.dev/docs/get-started/install)から)

**Start a new Flutter project**をクリック。

<img width="889" alt="スクリーンショット 2020-04-15 17.35.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/74daa733-1516-f43a-48ed-b31f80c398b1.png">

**Flutter Application**を選択し、**Next**をクリック。

<img width="884" alt="スクリーンショット 2020-04-16 12.20.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/9329d599-a08a-5834-b304-1bce554a9d26.png">

**Packeage Name**を入力し、**Finish**をクリック。。
※Packeage Nameは任意で良いです。

<img width="1012" alt="スクリーンショット 2020-04-15 17.38.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/b2d0442e-06d7-19c4-68e1-4548d5bf87b8.png">

プロジェクトが立ち上がったら、**pubspec.yamlのdependencies**に下記の文言を追加。
（Android StudioならShiftキーを２回押すとファイル検索できるので、**pubspec.yaml**と入力してください）

```pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  // この行を追加
  cloud_firestore: ^0.13.0+1
  // firebase_coreを使う場合、こちらの行に変更する
  // firebase_core: "^1.0.0"
  // cloud_firestore: "^1.0.0"

```

pubspec.yamlを編集したら、**flutter packages get**を実施するか、
Android Studioであれば画面右上の**Pub get**をクリック。

<img width="294" alt="スクリーンショット 2020-04-16 13.02.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/2504089b-fdcd-6666-e290-6b32bca87d56.png">

packagesをインストールし終えたら、Android emulatorでアプリを実行し
デフォルトのFlutter Appが立ち上がることを確認します。


<img width="363" alt="スクリーンショット 2020-04-16 13.23.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/7b808dce-d2fb-218a-f83e-6aa7efd8f170.png">

※**Cannot fit requested classes in a single dex fileエラー**が出た場合、minSdkVersionのバージョンを16->21に変更し、再度アプリを実行してください。

``` android/app/build.gradle
// minSdkVersion 16
minSdkVersion 21

```
## サンプルコードの実装

main.dartの既存コードを削除し、下記のコードを貼り付けます。
実行するとdummySnapshotの値がListViewで表示されるはずです。
コードの詳細は[元ページ](https://codelabs.developers.google.com/codelabs/flutter-firebase/index.html#4)を参照してください。

firebase_core利用なし

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

void main() => runApp(MyApp());

final dummySnapshot = [
 {"name": "Filip", "votes": 15},
 {"name": "Abraham", "votes": 14},
 {"name": "Richard", "votes": 11},
 {"name": "Ike", "votes": 10},
 {"name": "Justin", "votes": 1},
];

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Baby Names',
     home: MyHomePage(),
   );
 }
}

class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() {
   return _MyHomePageState();
 }
}

class _MyHomePageState extends State<MyHomePage> {
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(title: Text('Baby Name Votes')),
     body: _buildBody(context),
   );
 }

 Widget _buildBody(BuildContext context) {
   return _buildList(context, dummySnapshot);
 }

 Widget _buildList(BuildContext context, List<Map> snapshot) {
   return ListView(
     padding: const EdgeInsets.only(top: 20.0),
     children: snapshot.map((data) => _buildListItem(context, data)).toList(),
   );
 }

 Widget _buildListItem(BuildContext context, Map data) {
   final record = Record.fromMap(data);

   return Padding(
     key: ValueKey(record.name),
     padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
     child: Container(
       decoration: BoxDecoration(
         border: Border.all(color: Colors.grey),
         borderRadius: BorderRadius.circular(5.0),
       ),
       child: ListTile(
         title: Text(record.name),
         trailing: Text(record.votes.toString()),
         onTap: () => print(record),
       ),
     ),
   );
 }
}

class Record {
 final String name;
 final int votes;
 final DocumentReference reference;

 Record.fromMap(Map<String, dynamic> map, {this.reference})
     : assert(map['name'] != null),
       assert(map['votes'] != null),
       name = map['name'],
       votes = map['votes'];

 Record.fromSnapshot(DocumentSnapshot snapshot)
     : this.fromMap(snapshot.data, reference: snapshot.reference);

 @override
 String toString() => "Record<$name:$votes>";
}

```

firebase_core利用あり

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

void main() => runApp(MyApp());

final dummySnapshot = [
 {"name": "Filip", "votes": 15},
 {"name": "Abraham", "votes": 14},
 {"name": "Richard", "votes": 11},
 {"name": "Ike", "votes": 10},
 {"name": "Justin", "votes": 1},
];


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Baby Names',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() {
    return _MyHomePageState();
  }
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Baby Name Votes')),
      body: _buildBody(context),
    );
  }

  Widget _buildBody(BuildContext context) {
   return _buildList(context, dummySnapshot);
  }

  Widget _buildList(BuildContext context, List<Map> snapshot) {
    return ListView(
      padding: const EdgeInsets.only(top: 20.0),
      children: snapshot.map((data) => _buildListItem(context, data)).toList(),
    );
  }

  Widget _buildListItem(BuildContext context, Map data) {
    final record = Record.fromMap(data);

    return Padding(
      key: ValueKey(record.name),
      padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      child: Container(
        decoration: BoxDecoration(
          border: Border.all(color: Colors.grey),
          borderRadius: BorderRadius.circular(5.0),
        ),
        child: ListTile(
          title: Text(record.name),
          trailing: Text(record.votes.toString()),
          onTap: () => print(record),

        ),
      ),
    );
  }
}

class Record {
  final String name;
  final int votes;
  final DocumentReference reference;

  Record.fromMap(Map<String, dynamic> map, {this.reference})
      : assert(map['name'] != null),
        assert(map['votes'] != null),
        name = map['name'],
        votes = map['votes'];

  Record.fromSnapshot(DocumentSnapshot snapshot)
      : this.fromMap(snapshot.data(), reference: snapshot.reference);

  @override
  String toString() => "Record<$name:$votes>";
}

```

<img width="363" alt="スクリーンショット 2020-04-16 14.13.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/06cbb2bb-6912-828f-4365-b53ee64766e8.png">


## Firebaseにプロジェクト作成

**プロジェクトを作成**をクリック。
(アカウント作成は省略しますので必要に応じて行ってください。)

<img width="1213" alt="スクリーンショット 2020-04-15 19.07.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/fc3d0a69-2bfe-50e4-2bfe-c0962bd1d239.png">

**プロジェクト名**を入力。
※任意でOK。

<img width="1280" alt="スクリーンショット 2020-04-15 19.07.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/1c639fbf-3f6b-dab2-9c01-db6ff0a7d8ab.png">

下記の通り作成できればOKです。

<img width="1212" alt="スクリーンショット 2020-04-15 19.09.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/9feb3899-7ee9-ebf3-8b16-3c6f9b7b5af4.png">

### Androidアプリの設定
まずはAndroidの設定を追加します。
プロジェクトページ真ん中あたりの**ドロイド君**をクリック。

<img width="332" alt="スクリーンショット 2020-04-15 19.10.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/aec02499-71ad-c9e6-3485-7714c3e98bc1.png">

**パッケージ名**を入力。
※パッケージ名はFlutter projectを作成した時のものです。
ex)```com.example.babynames```

忘れた場合、**android/app/src/main/AndroidManifest.xml**の一番上の方に記載されています。


<img width="591" alt="スクリーンショット 2020-04-15 19.18.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/6bbcc394-7ebf-0be4-57c8-f472f65bb042.png">

※ Google sign-inを使いたい場合、
**デバッグ用の署名証明書 SHA-1**も入力しておく必要があるみたいです。
ターミナルで下記のコマンドを実行すると
SHA1: DA:39:A3:EE:5E:6B:4B:0D:32:55:BF:EF:95:60:18:90:AF:D8:07:09 のように取得できるので、必要に応じて入力してください。
入力するとパスワードを聞かれますが、初期パスワードは**android**です。

```.sh
keytool -list -v \
-alias androiddebugkey -keystore ~/.android/debug.keystore
```

アプリを登録したら、次は**google-services.jsonをダウンロード**します。

<img width="753" alt="スクリーンショット 2020-04-15 19.19.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/6775567f-4525-9e13-bc64-f8c77bd9cc46.png">

ダウンロードしたgoogle-services.jsonをプロジェクトの**android/app配下**に配置。
(Android Studioを開き、android/appの部分にドラッグ&ドロップでOK)

google-services.jsonを配置したら、
***android/app/build.gradle***と***android/build.gradle***をそれぞれ編集。

```android/app/build.gradle
dependencies {
    // dependencies内に下記の一行を追加
    implementation 'com.google.firebase:firebase-analytics:17.2.2'
}
 ...
// ファイルの一番下に下記の一行を追加
apply plugin: 'com.google.gms.google-services' 
``` 

```android/build.gradle
buildscript {
 ...
    dependencies {
         ...
        // 下記の文言を追加
        classpath 'com.google.gms:google-services:4.3.3'
    }
}
```

Androidエミュレータを起動し、下記の画面が表示されることを確認する。

<img width="265" alt="スクリーンショット 2020-04-16 11.07.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/3be5c731-fb16-d032-9341-9ad6e6078ca3.png">

Firebaseも下記のような表示になるはずです。

<img width="797" alt="スクリーンショット 2020-04-16 10.43.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/9d7620ad-287c-0674-7bba-4b8374e5ccf7.png">


### iOSアプリの設定
次にiOSの設定を追加します。
プロジェクトページ真ん中あたりの**iOS**をクリック。

<img width="332" alt="スクリーンショット 2020-04-15 19.10.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/aec02499-71ad-c9e6-3485-7714c3e98bc1.png">

**bundle id**を入力。
※bundle idはAndroidのパッケージ名と同じです。
ex)```com.example.babynames```

<img width="753" alt="スクリーンショット 2020-04-15 19.26.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/8f8dbf6d-e644-ce3a-0c70-21e06753be28.png">

アプリを登録したら、次は**GoogleService-Info.plistをダウンロード**します。

<img width="753" alt="スクリーンショット 2020-04-15 19.26.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/68e97f83-319e-d02a-cf64-6800832462e5.png">

ダウンロードしたら、ターミナルで下記のコマンドを入力し、Xcodeでプロジェクトを開きます。

```.sh
open {プロジェクトファイルパス}/ios/Runner.xcworkspace
```

開いたら、まずはbundle idが正しく入力されているか確認します。
（bundle identifierが正しいことを確認してください）

<img width="1005" alt="スクリーンショット 2020-04-16 10.00.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/80af4bd6-f035-ad31-0952-92f474f93261.png">

次にダウンロードしたGoogleService-Info.plistを**Runner/Runner配下**に配置。
(Runner/Runner直下にドラッグ&ドロップでOK)

<img width="727" alt="スクリーンショット 2020-04-16 10.05.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/b4d272c2-446c-80a0-e375-2cc20e758281.png">

配置後は下記のようになります。

<img width="272" alt="スクリーンショット 2020-04-16 10.06.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/05205723-4e13-a7ae-40e2-6eb01b7f1281.png">

iOSエミュレータを起動し、Androidと同じ画面が表示されていることを確認する。

<img width="317" alt="スクリーンショット 2020-04-16 16.03.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/4539177e-eb73-c831-39ba-54950e0a2268.png">

<img width="807" alt="スクリーンショット 2020-04-16 10.48.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/953eb504-6ac8-a796-f3fb-3ea6d3e7b20c.png">


## Databaseの作成

投票数をアプリで共有するためにDatabaseを作成します。
左ペインから**Database**を選択。

<img width="250" alt="スクリーンショット 2020-04-16 10.53.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/e8e0d71e-574a-a882-4bb2-528bb70c5a6e.png">

**データベースの作成**をクリック。

<img width="448" alt="スクリーンショット 2020-04-16 10.53.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/06d74d84-92f3-7486-bd7f-3af1f55f055c.png">

**テストモードで開始**を選択し、**次へ**をクリック。

<img width="805" alt="スクリーンショット 2020-04-16 10.54.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/b0090c95-aa0f-258c-1a70-c685deec01c9.png">

**Cloud Firestoreのロケーション**を選択。
※任意でOK

<img width="800" alt="スクリーンショット 2020-04-16 10.54.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/607f7fda-c0c8-e8cb-583f-e04d9ff04cb8.png">

**コレクションを開始**をクリックし、**コレクションID**を入力。
※任意でOK（コレクションIDは後で使うので覚えておいてください）

<img width="689" alt="スクリーンショット 2020-04-16 10.56.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/40c148f7-9aad-ea9a-dfb5-76f92637f50e.png">

リスト作成用のデータを登録していく。
詳細はキャプチャ参照

<img width="505" alt="スクリーンショット 2020-04-16 16.39.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/71295fb1-e115-6bea-b114-ad3a90a770ca.png">

適当な数を追加し、下記のような形にします。

<img width="780" alt="スクリーンショット 2020-04-16 11.02.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116581/7d87b5e8-f4a2-f949-6472-e1bf074e79ac.png">

## 投票アプリの実装

Android Studioを開き、main.dartの下記の部分を編集します。

firebase_core利用なし

```main.dart

// 削除
// final dummySnapshot = [
//  {"name": "Filip", "votes": 15},
//  {"name": "Abraham", "votes": 14},
//  {"name": "Richard", "votes": 11},
//  {"name": "Ike", "votes": 10},
//  {"name": "Justin", "votes": 1},
// ];

Widget _buildBody(BuildContext context) {
//   return _buildList(context, dummySnapshot);  削除
  return StreamBuilder<QuerySnapshot>(
    stream: Firestore.instance.collection('baby').snapshots(), // babyは各々のコレクションIDに変更してください
    builder: (context, snapshot) {
      if (!snapshot.hasData) return LinearProgressIndicator();

      return _buildList(context, snapshot.data.documents);
    },
  );
}

//  Widget _buildList(BuildContext context, List<Map> snapshot) { 変更
Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
  return ListView(
    padding: const EdgeInsets.only(top: 20.0),
    children: snapshot.map((data) => _buildListItem(context, data)).toList(),
  );
}

//  Widget _buildListItem(BuildContext context, Map data) { 変更
Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
// final record = Record.fromMap(data);　変更
  final record = Record.fromSnapshot(data);

  return Padding(
    key: ValueKey(record.name),
    padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
    child: Container(
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey),
        borderRadius: BorderRadius.circular(5.0),
      ),
      child: ListTile(
        title: Text(record.name),
        trailing: Text(record.votes.toString()),
//      onTap: () => print(record),　変更
        onTap: () => record.reference.updateData({'votes': FieldValue.increment(1)}),
      ),
    ),
  );
}
```

firebase_core利用あり

```main.dart

// 削除
// final dummySnapshot = [
//  {"name": "Filip", "votes": 15},
//  {"name": "Abraham", "votes": 14},
//  {"name": "Richard", "votes": 11},
//  {"name": "Ike", "votes": 10},
//  {"name": "Justin", "votes": 1},
// ];

  Widget _buildBody(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
      stream: FirebaseFirestore.instance.collection('baby').snapshots(), // babyは各々のコレクションIDに変更してください
      builder: (context, snapshot) {
        if (!snapshot.hasData) return LinearProgressIndicator();

        return _buildList(context, snapshot.data.docs);
      },
    );
  }

  //  Widget _buildList(BuildContext context, List<Map> snapshot) { 変更
  Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) {
    return ListView(
      padding: const EdgeInsets.only(top: 20.0),
      children: snapshot.map((data) => _buildListItem(context, data)).toList(),
    );
  }

//  Widget _buildListItem(BuildContext context, Map data) { 変更
  Widget _buildListItem(BuildContext context, DocumentSnapshot data) {
// final record = Record.fromMap(data);　変更
    final record = Record.fromSnapshot(data);

    return Padding(
      key: ValueKey(record.name),
      padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      child: Container(
        decoration: BoxDecoration(
          border: Border.all(color: Colors.grey),
          borderRadius: BorderRadius.circular(5.0),
        ),
        child: ListTile(
          title: Text(record.name),
          trailing: Text(record.votes.toString()),
//      onTap: () => print(record),　変更
          onTap: () => record.reference.update({'votes': FieldValue.increment(1)}),
        ),
      ),
    );
  }
}
```

以上で完了です。
アプリを起動し、Databaseとアプリのデータが連動していることを確認してみてください。