【Flutter/Dart】Firebase/Firestoreを使ったチャットアプリを作ろう 

# 1.はじめに
新たに6つのAndroidStudio&Flutter&Firebase記事を書いている。以下は内容、
- 【Flutter/Dart】Firebaseを使おう
https://qiita.com/my_programming/private/7b3251701183ff59b97c
- 【Flutter/Dart】Firebase Authenticationを使って認証アプリを作ろう
https://qiita.com/my_programming/private/2d81229942bc0d61da6b
- **【Flutter/Dart】Firebase/Firestoreを使ったチャットアプリを作ろう**
- 【Flutter/Dart】Firebase/Firestoreを使ったToDoアプリを作ろう
https://qiita.com/my_programming/private/2da77f3a36bf120761c5
- 【Flutter/Dart】Firebase/HostingでWebアプリを外部公開しよう
https://qiita.com/my_programming/private/5950b7e27161dc9ec838
- 【Flutter/Dart】Firebase/Firestoreを使ったBlogアプリを作ろう
https://qiita.com/my_programming/private/6f8515dca825e55d0f07

今回はバックエンドにFirebaseを搭載したFlutterでわかりやすく複雑なチャットアプリケーションを作成する。

# 2. チャットアプリを作ろう 
Firebaseのプロジェクトについては認証アプリで作成した。同じプロジェクトのページを開き、Cloud Firestoreを選択する。
<img width="400" alt="step2_1_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/d5c37688-f66f-ba35-95fc-103b097d8bcd.png">

以下のページで`データベースの作成`を押す。
<img width="400" alt="step2_1_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/77199dc3-363d-0de9-08c3-ca85d6b20ae4.png">

以下のページで`テストモードを開始する`を選択し、`次へ`を押す。
<img width="400" alt="step2_1_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/55516b41-98aa-68ab-3b9e-e872349c138d.png">

`有効にする`を押す。
<img width="400" alt="step2_1_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/d0a52ec3-abf7-6537-9102-c750725e5c6b.png">

以下の画面が出たら暫く待つ。
<img width="400" alt="step2_1_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/7d9fbb08-395f-12cc-e3f5-c8e32bc4ff85.png">

以下の画面が表示されたらクラウド側の準備は完了。
<img width="400" alt="step2_1_6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/c8a77fe8-49bd-373a-90bf-aba399098b77.png">

AndroidStudioにて、チャットアプリを作るための新規プロジェクトを作成する。
<img width="400" alt="step2_1_7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/6f4a37ac-762f-20cd-2cb5-6fe7494f9af4.png">


以下は認証アプリと同様に、classpathをandroid/build.gradleの中にコピーする。
<img width="400" alt="step2_1_9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/abf2a281-ad22-69d5-bfe1-1d2182d5ee1f.png">

以下は認証アプリと同様に、`android/app/build.gradle`にidをペーストして`apply plugin: 'com.google.gms.google-services'`の通りに書き換える。
<img width="400" alt="step2_1_11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/a5ab6145-d86d-110b-6c40-fb6cdd39e2ab.png">

以下は認証アプリと同様に、dependenciesの中のimplementationの2行をコピーしてandroid/app/build.gradleにペーストする。
<img width="400" alt="step2_1_14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/c4d33355-4051-6891-f096-fc40756de78c.png">

以下は認証アプリと同様に、2つのパッケージを`dart package`で検索、コピーしたバージョン情報をpubspec.yamlにペースト、右上のPub getを押す。
<img width="400" alt="step2_1_8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/29103cae-5cde-161f-28ad-fd9cac72eb45.png">

`applicationId`が"com.example.my_chat_app"になっていることを確認する。
<img width="400" alt="step2_1_12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/986c129f-a380-fc95-ab4b-82b27bc90c76.png">

以下は認証アプリと同様に、`android/app/build.gradle`のminSdkVersionを23に、targetSdkVersionを30に変更する。`applicationId`を"com.example.my_auth_app"に書き換える。これで、認証アプリと同じFirebaseプロジェクトのデータベースを利用できる様になる。
<img width="400" alt="step2_1_13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/bf644d07-f0ed-2726-27bb-b2106ae777c0.png">

lib/main.dartにチャット用のコードを記述。

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Chat App',
      theme: new ThemeData(
          primaryColor: Colors.red
      ),
      home: new UserPage(),
    );
  }
}

class UserPage extends StatefulWidget {

  @override
  _UserPageState createState() => new _UserPageState();
}

class _UserPageState extends State<UserPage> {

  final _controller = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("User Name Input Screen"),
          backgroundColor: Colors.red
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Container(
            margin: EdgeInsets.all(12.0),
            child: TextFormField(
              controller: _controller,
              decoration: InputDecoration(
                  labelText: 'User Name'
              ),
            ),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.push(
                  context, MaterialPageRoute(builder: (builder) => ChatPage(_controller.text)));
            },
            child: Text("Setup complete", style: TextStyle(color: Colors.white),),
          )
        ],
      ),
    );
  }
}


class ChatPage extends StatefulWidget {
  ChatPage(this._userName);

  final String _userName;

  @override
  _ChatPageState createState() => new _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text("chat page"),
            backgroundColor: Colors.red
        ),
        body: Container(
          margin: const EdgeInsets.symmetric(horizontal: 10.0),
          child: Column(
            children: <Widget>[
              Flexible(
                child: StreamBuilder<QuerySnapshot>(
                  stream: FirebaseFirestore.instance
                      .collection("ChatApp")
                      .orderBy("time", descending: true)
                      .snapshots(),
                  builder: (context, snapshot) {
                    if (!snapshot.hasData) return Container();
                    return new ListView.builder(
                      padding: new EdgeInsets.all(8.0),
                      reverse: true,
                      itemBuilder: (_, int index) {
                        DocumentSnapshot document =
                        snapshot.data!.docs[index];

                        bool isSelfText = false;
                        if (document['username'] == widget._userName) {
                          isSelfText = true;
                        }
                        return isSelfText
                            ? _SelfText(
                            document['sent_text'], document['username'])
                            : _OtherText(
                            document['sent_text'], document['username']);
                      },
                      itemCount: snapshot.data!.docs.length,
                    );
                  },
                ),
              ),
              new Divider(height: 1.0),
              Container(
                margin: EdgeInsets.only(bottom: 20.0, right: 10.0, left: 10.0),
                child: Row(
                  children: <Widget>[
                    new Flexible(
                      child: new TextField(
                        controller: _controller,
                        onSubmitted: _handleSubmit,
                        decoration:
                        new InputDecoration.collapsed(hintText: "send text"),
                      ),
                    ),
                    new Container(
                      child: new IconButton(
                          icon: new Icon(
                            Icons.chat,
                            color: Colors.red,
                          ),
                          onPressed: () {
                            _handleSubmit(_controller.text);
                          }),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ));
  }

  Widget _SelfText(String message, String userName) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.end,
      children: <Widget>[
        Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(height: 12.0,),
            Text(userName, style: TextStyle(color: Colors.red)),
            Text(message),
          ],
        ),
        Icon(Icons.person, color:Colors.red),
      ],
    );
  }

  Widget _OtherText(String message, String userName) {
    return Row(
      children: <Widget>[
        Icon(Icons.person_outline),
        Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(height: 12.0,),
            Text(userName),
            Text(message),
          ],
        )
      ],
    );
  }

  _handleSubmit(String message) {
    _controller.text = "";
    var db = FirebaseFirestore.instance;
    db.collection("ChatApp").add({
      "username": widget._userName,
      "sent_text": message,
      "time": DateTime.now()
    }).then((val) {
      print("Success");
    }).catchError((err) {
      print(err);
    });
  }
}
```

まずユーザー1の名前を入力する。
<img width="200" alt="step2_1_15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/348b86ef-ed64-14c1-ce87-1286f25ad97a.png">

次にユーザー1のままテキストを投稿する。
<img width="200" alt="step2_1_16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/958b7ef1-2d81-2974-2679-95ce7d1d47ef.png">

次に前の画面に戻ってユーザー2の名前を入力し、ユーザー2のテキストを投稿する。
<img width="200" alt="step2_1_17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/9414ebb9-7bc7-1734-797d-14a48d73ed88.png">

ユーザー2名分のテキストが、Cloud Firestoreのデータベースに反映されていることが確認できる。
<img width="400" alt="step2_1_18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/779f4443-e237-9756-6733-ff8fa90e6f00.png">

以上で、FlutterとFirebaseを用いたチャットアプリが完成した。

# 3.まとめ
FlutterとFirebaseを用いたチャットアプリの作成方法を学んだ。