🙄
flutter × firebaseを使用したチャットアプリの開発
ここではflutterとfirebaseを使用したチャットアプリの開発をしていきます。
前提
チャット機能を開発する上で以下のことを考えて作業を行います。
- 使用するアーキテクチャ - MVC
- M - Model
- V - View
- C - Controller
- 作業フロー
- Modelの作成
- Messageを表示するコンポーネントの作成
- メッセージを送信する際のfirebaseとのやりとり(Controller)実装
- Viewの作成
Modelの作成
message.dart
import 'package:cloud_firestore/cloud_firestore.dart';
class Message {
final String senderId;
final String senderEmail;
final String receiverId;
final String message;
final Timestamp timestamp;
Message({
required this.senderId,
required this.senderEmail,
required this.receiverId,
required this.message,
required this.timestamp,
});
Map<String, dynamic> toMap() {
return {
'senderId': senderId,
'senderEmail': senderEmail,
'receiverId': receiverId,
'message': message,
'timestamp': timestamp,
};
}
}
上記のクラスのフィールドを一つずつ説明します。
- senderId - メッセージを送信した人のuid
- senderEmail - メッセージを送信した人のメールアドレス
- receiverId - 返信する人(相手)のuid
- message - メッセージ内容
- timestamp - メッセージの送信時間
上記のモデル(設計)を使用してメッセージの送信を行います。
本来はsenderEmailの箇所をその人のユーザネームに置き換えます。
Messageを表示するコンポーネントの作成
message_tile.dart
import 'package:flutter/material.dart';
class MessageTile extends StatefulWidget {
final String message;
final bool sentByMe;
final String time;
const MessageTile(
{Key? key,
required this.message,
required this.sentByMe,
required this.time})
: super(key: key);
State<MessageTile> createState() => _MessageTileState();
}
class _MessageTileState extends State<MessageTile> {
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: 4,
bottom: 4,
left: widget.sentByMe ? 0 : 24,
right: widget.sentByMe ? 24 : 0),
alignment: widget.sentByMe ? Alignment.centerRight : Alignment.centerLeft,
child: Column(
crossAxisAlignment:
widget.sentByMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Container(
margin: widget.sentByMe
? const EdgeInsets.only(left: 30)
: const EdgeInsets.only(right: 30),
padding:
const EdgeInsets.only(top: 17, bottom: 17, left: 20, right: 20),
decoration: BoxDecoration(
borderRadius: widget.sentByMe
? const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
bottomLeft: Radius.circular(20),
)
: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
bottomRight: Radius.circular(20),
),
color: widget.sentByMe
? Theme.of(context).primaryColor
: Colors.grey[700]),
child: Text(
widget.message,
textAlign: TextAlign.start,
style: TextStyle(
fontSize: 16, color: Theme.of(context).colorScheme.onPrimary),
),
),
Text(
widget.time,
style: TextStyle(
fontSize: 7, color: Theme.of(context).colorScheme.onSecondary),
)
],
),
);
}
}
上記のコンポーネントのフィールドは以下
- message - メッセージ内容
- sentByme - メッセージの送信者が自分であるかそうでないか
- time - 送信した時間
上記のコンポーネントは送信者が自分である場合にはmessageを右側に、そうでない場合にはmessageを左側に持ってきます。
そしてそのメッセージの下に投稿した時間が表示されるようにしています。
メッセージを送信する際のfirebaseとのやりとり(Controller)実装
ここではfirebaseにメッセージの登録を実際に行なっていきます。
chat_services.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_timelines/models/message.dart';
class ChatService extends ChangeNotifier {
// get instance of auth auth and firestore
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
final FirebaseFirestore _fireStore = FirebaseFirestore.instance;
// SEND MESSAGE
Future<void> sendMessage(String receiverId, String message) async {
//get currentUser info
final String currentUserId = _firebaseAuth.currentUser!.uid;
final String currentUserEmail = _firebaseAuth.currentUser!.email.toString();
final Timestamp timestamp = Timestamp.now();
// create a new message
Message newMessage = Message(
senderId: currentUserId,
senderEmail: currentUserEmail,
receiverId: receiverId,
message: message,
timestamp: timestamp,
);
//construct chat room id from current User id and receiver id (sorted to ensure uniqueness)
List<String> ids = [currentUserId, receiverId];
ids.sort(); // sort the ids (this ensures the chat room id is always the same for any pair of people)
String chatRoomId = ids.join(
"_"); // combine the ids info a single string to use as a chatroomID
// add new message to database
await _fireStore
.collection('chat_rooms')
.doc(chatRoomId)
.collection('messages')
.add(newMessage.toMap());
}
// GET MESSAGES
Stream<QuerySnapshot> getMessages(String userId, String otherUserId) {
// construct chat room id from user ids (sorted to ensure it matches the id used when sending )
List<String> ids = [userId, otherUserId];
ids.sort();
String chatRoomId = ids.join("_");
return _fireStore
.collection('chat_rooms')
.doc(chatRoomId)
.collection("messages")
.orderBy('timestamp', descending: false)
.snapshots();
}
}
上記のコントローラには以下の関数を配置しています。
- sendMessage - firebaseに送信した内容を登録する
- getMessage - firebaseに登録している内容を取得する
それぞれ具体的に説明をします
- sendMessage
前提として登録する内容はmessage.dartで紹介しているフィールドです。
sendMessageには以下の二つの引数を配置しています。
- receiverId - 相手のuid
- message - メッセージ内容
そしてMessageモデルを使用してメッセージの登録を行います。
Message newMessage = Message(
senderId: currentUserId,
senderEmail: currentUserEmail,
receiverId: receiverId,
message: message,
timestamp: timestamp,
);
メッセージの登録作業
// add new message to database
await _fireStore
.collection('chat_rooms')
.doc(chatRoomId)
.collection('messages')
.add(newMessage.toMap());
- getMessage
chatPageに表示する内容をここで取得します。
List<String> ids = [userId, otherUserId];
ids.sort();
ここで自分のIdと相手のIdを取得します。
そしてsortをして並び替え。
return _fireStore
.collection('chat_rooms')
.doc(chatRoomId)
.collection("messages")
.orderBy('timestamp', descending: false)
.snapshots();
ここで一つ一つの投稿内容を取得していきます。
これらの関数は次のchat_page.dartで使用します。
chat_page.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_timelines/helper/helper_methods.dart';
import 'package:flutter_timelines/controller/chat_service.dart';
import 'package:flutter_timelines/view/components/message_tile.dart';
import 'package:flutter_timelines/view/components/chat_text_field.dart';
class ChatPage extends StatefulWidget {
final String uid;
final String username;
const ChatPage({
Key? key,
required this.uid,
required this.username,
}) : super(key: key);
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final TextEditingController _messageController = TextEditingController();
final ChatService _chatService = ChatService();
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
// for textfield focus
FocusNode myFocusNode = FocusNode();
void initState() {
super.initState();
// add listener to focus node
myFocusNode.addListener(() {
if (myFocusNode.hasFocus) {
// cause a delay so that the keyboard has time to show up
// then the amount of remaining space will be calculated
//then scroll down
Future.delayed(const Duration(milliseconds: 500), () => scrollDown());
}
});
// wait a bit for listview to be built, then scroll to bottom
Future.delayed(const Duration(milliseconds: 50), () => scrollDown());
}
void dispose() {
myFocusNode.dispose();
_messageController.dispose();
super.dispose();
}
// scroll controller
final ScrollController _scrollController = ScrollController();
void scrollDown() async {
_scrollController.animateTo(_scrollController.position.maxScrollExtent,
duration: const Duration(seconds: 1), curve: Curves.fastOutSlowIn);
}
void sendMessage() async {
// only send message if there is something to send
if (_messageController.text.isNotEmpty) {
await _chatService.sendMessage(widget.uid, _messageController.text);
// clear the text controller after sending the message
_messageController.clear();
}
scrollDown();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.username),
),
body: Column(
children: [
//message
Expanded(
child: _buildMessageList(),
),
//user input
_buildMessageInput(),
const SizedBox(
height: 30,
)
],
),
);
}
// build message list
Widget _buildMessageList() {
return StreamBuilder(
stream: _chatService.getMessages(
widget.uid,
_firebaseAuth.currentUser!.uid,
),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text("Error${snapshot.error}");
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading...");
}
WidgetsBinding.instance?.addPostFrameCallback((_) => scrollDown());
return ListView.builder(
controller: _scrollController,
itemCount: snapshot.data!.docs.length,
itemBuilder: (context, index) {
return _buildmessageItem(snapshot.data!.docs[index]);
},
);
},
);
}
// build message item
Widget _buildmessageItem(DocumentSnapshot document) {
Map<String, dynamic> data = document.data() as Map<String, dynamic>;
return MessageTile(
message: data["message"],
sentByMe: data['senderId'] == _firebaseAuth.currentUser!.uid,
time: chatMessageDate(data['timestamp']),
);
}
// build message input
Widget _buildMessageInput() {
return Container(
width: 370,
child: Row(
children: [
// textfield
Expanded(
child: ChatTextField(
controller: _messageController,
hintText: '入力',
obscureText: false,
focusNode: myFocusNode,
),
),
//send button
IconButton(
onPressed: sendMessage,
icon: Container(
width: 50,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: const LinearGradient(
colors: [Colors.deepPurple, Colors.blue], // グラデーションの色のリスト
begin: Alignment.centerLeft, // グラデーションの開始位置
end: Alignment.centerRight, // グラデーションの終了位置
),
),
child: Center(
child: Icon(
Icons.send,
size: 25,
color: Theme.of(context).colorScheme.primary,
),
),
),
)
],
),
);
}
}
上記ではChatをするページの作成をしています。
Discussion