Flutter×Firebaseでチャットアプリを作る

なんとなくFlutterの勉強がてら作りました。
基本的に以下のサイト様を参考にさせていただいたので詳しく知りたい方は↓を見てください。

https://qiita.com/atm_33/items/0b066c34280b39628910

https://zenn.dev/tama8021/articles/0816_flutter_chat

# Firebaseの準備
以下のサイトの２章「Flutter × Firebase のセットアップ」を参考にFirebaseとFlutterをセットアップします。

https://blog.flutteruniv.com/flutter-firebase/

# さっそく実装
今回作成するのは以下の3つです。（ボトムメニューは別です。）
+ チャットルーム一覧
+ チャットルーム新規作成
+ チャットルーム本体

![Simulator Screen Shot - iPhone SE (3rd generation) - 2023-01-07 at 21.09.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2658497/745ec35f-9554-09e7-82f1-153408a72370.png)
![Simulator Screen Shot - iPhone SE (3rd generation) - 2023-01-07 at 21.37.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2658497/a542dd5b-dd8b-8045-fadf-96f8b07fdfef.png)
![Simulator Screen Shot - iPhone SE (3rd generation) - 2023-01-07 at 21.09.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2658497/f0976a76-3171-deff-ec26-7a242948beb2.png)

## チャットルーム一覧
```dart:chat_room.dart
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_app/src/screens/home.dart';

import 'chat_room.dart';
import 'add_room_page.dart';

class RoomListPage extends StatelessWidget {
  const RoomListPage();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('一覧'),
      ),
      body: Center(
        child: StreamBuilder<QuerySnapshot>(
            stream: FirebaseFirestore.instance
              .collection('chat_room')
              .orderBy('createdAt')
              .snapshots(),
            builder: (context, snapshot) {
              if (!snapshot.hasData) {
                return Center(
                  child: Text('読込中……'),
                );
              }
              else if (snapshot.hasData) {
                final List<DocumentSnapshot> documents = snapshot.data!.docs;
                return ListView.builder(
                  itemCount: documents.length,
                  itemBuilder: (BuildContext context, int index){
                    return Card(
                      child: ListTile(
                        title: Text(documents[index]['name']),
                        trailing: IconButton(
                          icon: Icon(Icons.input),
                          onPressed: () async {
                            await Navigator.of(context).push(
                              MaterialPageRoute(builder: (context) {
                                return ChatRoom(documents[index]['name']);
                              },
                              ),
                            );
                          },
                        ),
                      ),
                    );
                  }
                );
              }
              else{
                return Center(
                  child: Text('なし'),
                );
              }
            },
          ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () async {
          await Navigator.of(context)
              .push(MaterialPageRoute(builder: (context) {
            return AddRoomPage();
          }));
        },
      ),
    );
  }
}
```

## チャットルーム作成
```dart:add_room_page.dart
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class AddRoomPage extends StatefulWidget {
  AddRoomPage();

  @override
  _AddPostPageState createState() => _AddPostPageState();
}

class _AddPostPageState extends State<AddRoomPage> {
  String roomName = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ルーム作成'),
      ),
      body: Center(
        child: Container(
          padding: EdgeInsets.all(32),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextFormField(
                decoration: InputDecoration(labelText: 'チャットルーム名'),
                keyboardType: TextInputType.multiline,
                maxLines: 3,
                onChanged: (String value) {
                  setState(() {
                    roomName = value;
                  });
                },
              ),
              const SizedBox(
                height: 8,
              ),
              Container(
                width: double.infinity,
                child: ElevatedButton(
                  child: Text('新規作成'),
                  onPressed: () async {
                    final date = DateTime.now().toLocal().toIso8601String();

                    await FirebaseFirestore.instance
                        .collection('chat_room')
                        .doc(roomName)
                        .set({
                      'name': roomName,
                      'createdAt': date,
                    });
                    Navigator.of(context).pop();
                  },
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
```

## チャットルーム本体
```dart:chat_room.dart
import 'dart:convert';
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';

String randomString() {
  final random = Random.secure();
  final values = List<int>.generate(16, (i) => random.nextInt(255));
  return base64UrlEncode(values);
}

class ChatRoom extends StatefulWidget {
  const ChatRoom(this.name, {Key? key}) : super(key: key);

  final String name;
  @override
  ChatRoomState createState() => ChatRoomState();
}

class ChatRoomState extends State<ChatRoom> {
  List<types.Message> _messages = [];
  final _user = const types.User(id: '82091008-a484-4a89-ae75-a22bf8d6f3ac');

  void initState() {
    _getMessages();
    super.initState();
  }

  void _getMessages() async {
    final getData = await FirebaseFirestore.instance
        .collection('chat_room')
        .doc(widget.name)
        .collection('contents')
        .orderBy('createdAt', descending: true)
        .get();

    final message = getData.docs
        .map((d) =>
        types.TextMessage(
            author:
            types.User(id: d.data()['uid'], firstName: d.data()['name']),
            createdAt: d.data()['createdAt'],
            id: d.data()['id'],
            text: d.data()['text']))
        .toList();

    setState(() {
      _messages = [...message];
    });
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: Text("チャットルーム"),
    ),
    body: Chat(
      user: _user,
      messages: _messages,
      onSendPressed: _handleSendPressed,
    ),
  );

  void _addMessage(types.TextMessage message) async {
    setState(() {
      _messages.insert(0, message);
    });

    await FirebaseFirestore.instance
        .collection('chat_room')
        .doc(widget.name)
        .collection('contents')
        .add({
      'uid': message.author.id,
      'name': message.author.firstName,
      'createdAt': message.createdAt,
      'id': message.id,
      'text': message.text,
    });
  }

  void _handleSendPressed(types.PartialText message) {
    final textMessage = types.TextMessage(
      author: _user,
      createdAt: DateTime.now().millisecondsSinceEpoch,
      id: randomString(),
      text: message.text,
    );
    _addMessage(textMessage);
  }
}
```

# おわりに
一通り動いたらFirebaseのFireStore Databaseも確認してみてください。
中にはチャットの内容が保存されているはずです。

## 躓いたところ
firebase上のデータが何らかの不手際でおかしくなっており、そのせいで長い間エラーが解決しなかった。
コードは間違ってなそうなのに動かない時は、FirebaseのDB上のデータやセキュリティルールを確認してみると良いかも。