1. 채팅을 입력하면 화면에 나오게 세팅해보자.
먼저 데이터를 파베로 보내고, 해당 파베 데이터를 가지고 와서 화면에 보여주는식으로 진행할 예정이다.
- new_message.dart
import 'package:cloud_firestore/cloud_firestore.dart' ;
import 'package:firebase_auth/firebase_auth.dart' ;
import 'package:flutter/material.dart' ;
class NewMessage extends StatefulWidget {
const NewMessage ({ super . key}) ;
@override
State < NewMessage > createState () {
return _NewMessageState () ;
}
}
class _NewMessageState extends State < NewMessage > {
final _messageController = TextEditingController () ;
@override
void dispose () {
_messageController . dispose () ;
super . dispose () ;
}
void _submitMessage () async {
final enteredMessage = _messageController . text ;
if (enteredMessage . trim () . isEmpty) {
return ;
}
FocusScope . of (context) . unfocus () ;
_messageController . clear () ;
final user = FirebaseAuth . instance . currentUser ! ;
final doc = await FirebaseFirestore . instance
. collection ( 'users' )
. doc (user . uid)
. get () ;
final userData = doc . data () as Map < String , dynamic > ;
FirebaseFirestore . instance . collection ( 'chat' ) . add ({
'text' : enteredMessage ,
'createdAt' : Timestamp . now () ,
'userId' : user . uid ,
'username' : userData[ 'username' ] ,
'userImage' : userData[ 'image_url' ] ,
}) ;
}
@override
Widget build ( BuildContext context) {
return Padding (
padding : const EdgeInsets . only (left : 15 , right : 1 , bottom : 14 ) ,
child : Row (
children : [
Expanded (
child : TextField (
controller : _messageController ,
textCapitalization : TextCapitalization . sentences ,
autocorrect : true ,
enableSuggestions : true ,
decoration : const InputDecoration (labelText : 'Send a message...' ) ,
) ,
) ,
IconButton (
color : Theme . of (context) . colorScheme . primary ,
icon : const Icon (
Icons . send ,
) ,
onPressed : _submitMessage ,
) ,
] ,
) ,
) ;
}
}
1) textField를 사용하기 위해 컨트롤러 세팅 및 dispose
2) submit이 되면 textfield에 입력한 키보드를 내려주고, text를 지워준다. 그 후, 현재 로그인된 계정id로 파베스토어에 저장한 데이터를 불러온다. 그리고, 파베스토어에 해당 유저정보 + 새로 입력한 text를 넣어준다.
3) final userData = doc.data() as Map<String, dynamic>; 공홈에 이렇게 하라고 나와있다. 안하면 에러난다.
- chat_messages.dart
import 'package:chat_app/widgets/message_bubble.dart' ;
import 'package:cloud_firestore/cloud_firestore.dart' ;
import 'package:firebase_auth/firebase_auth.dart' ;
import 'package:flutter/material.dart' ;
class ChatMessages extends StatelessWidget {
const ChatMessages ({ super . key}) ;
@override
Widget build ( BuildContext context) {
final authenticatedUser = FirebaseAuth . instance . currentUser ! ;
return StreamBuilder (
stream : FirebaseFirestore . instance
. collection ( 'chat' )
. orderBy (
'createdAt' ,
descending : true ,
)
. snapshots () ,
builder : (ctx , chatSnapshots) {
if (chatSnapshots . connectionState == ConnectionState . waiting) {
return const Center (
child : CircularProgressIndicator () ,
) ;
}
if ( ! chatSnapshots . hasData || chatSnapshots . data ! . docs . isEmpty) {
return const Center (
child : Text ( 'No messages found.' ) ,
) ;
}
if (chatSnapshots . hasError) {
return const Center (
child : Text ( 'Something went wrong...' ) ,
) ;
}
final loadedMessages = chatSnapshots . data ! . docs ;
return ListView . builder (
padding : const EdgeInsets . only (
bottom : 40 ,
left : 13 ,
right : 13 ,
) ,
reverse : true ,
itemCount : loadedMessages . length ,
itemBuilder : (ctx , index) {
final chatMessage = loadedMessages[index] . data () ;
final nextChatMessage = index + 1 < loadedMessages . length
? loadedMessages[index + 1 ] . data ()
: null ;
final currentMessageUserId = chatMessage[ 'userId' ] ;
final nextMessageUserId =
nextChatMessage != null ? nextChatMessage[ 'userId' ] : null ;
final nextUserIsSame = nextMessageUserId == currentMessageUserId ;
if (nextUserIsSame) {
return MessageBubble . next (
message : chatMessage[ 'text' ] ,
isMe : authenticatedUser . uid == currentMessageUserId ,
) ;
} else {
return MessageBubble . first (
userImage : chatMessage[ 'userImage' ] ,
username : chatMessage[ 'username' ] ,
message : chatMessage[ 'text' ] ,
isMe : authenticatedUser . uid == currentMessageUserId ,
) ;
}
} ,
) ;
} ,
) ;
}
}
1) 불러온 채팅 데이터를 화면에 보여주는 위젯임
2) StreamBuilder는 내가 보고 있는 데이터의 변화가 생기면 실시간으로 업데이트해주는 역할이다(리스너). (그래서 stateless사용)
그리고, 가장 최신 채팅글이 하단에 위치하기 위해서 내림차순으로 데이터를 읽어온다. (이전에 했었던 futurebuilder는 한번만 값을 가져오는것임)
3) 로딩 스피너, 에러 처리를 세팅하고, 데이터를 받아오면 loadedMessages 에 담는다. 그리고 리스트 뷰를 통해 하나씩 text값을 보여주는 로직이다.
4) 첫 메시지인경우 프로필 사진과 유저네임이 나와야하고, 말풍썬 끝이 올라가있는 상태를 구현해주어야한다. 그리고 동일유저가 계속 채팅을 이어서 쓸 경우에는 프로필 사진과 유저네임 없이 이어서 나오게 한다. 이를 구현하기 위해 메시지버블위젯을 만들어서 사용했다. next와 first로
-message_bubble.dart
import 'package:flutter/material.dart' ;
// A MessageBubble for showing a single chat message on the ChatScreen.
class MessageBubble extends StatelessWidget {
// Create a message bubble which is meant to be the first in the sequence.
const MessageBubble . first ({
super . key ,
required this . userImage ,
required this . username ,
required this . message ,
required this . isMe ,
}) : isFirstInSequence = true ;
// Create a amessage bubble that continues the sequence.
const MessageBubble . next ({
super . key ,
required this . message ,
required this . isMe ,
}) : isFirstInSequence = false ,
userImage = null ,
username = null ;
// Whether or not this message bubble is the first in a sequence of messages
// from the same user.
// Modifies the message bubble slightly for these different cases - only
// shows user image for the first message from the same user, and changes
// the shape of the bubble for messages thereafter.
final bool isFirstInSequence ;
// Image of the user to be displayed next to the bubble.
// Not required if the message is not the first in a sequence.
final String ? userImage ;
// Username of the user.
// Not required if the message is not the first in a sequence.
final String ? username ;
final String message ;
// Controls how the MessageBubble will be aligned.
final bool isMe ;
@override
Widget build ( BuildContext context) {
final theme = Theme . of (context) ;
return Stack (
children : [
if (userImage != null )
Positioned (
top : 15 ,
// Align user image to the right, if the message is from me.
right : isMe ? 0 : null ,
child : CircleAvatar (
backgroundImage : NetworkImage (
userImage ! ,
) ,
backgroundColor : theme . colorScheme . primary . withAlpha ( 180 ) ,
radius : 23 ,
) ,
) ,
Container (
// Add some margin to the edges of the messages, to allow space for the
// user's image.
margin : const EdgeInsets . symmetric (horizontal : 46 ) ,
child : Row (
// The side of the chat screen the message should show at.
mainAxisAlignment :
isMe ? MainAxisAlignment . end : MainAxisAlignment . start ,
children : [
Column (
crossAxisAlignment :
isMe ? CrossAxisAlignment . end : CrossAxisAlignment . start ,
children : [
// First messages in the sequence provide a visual buffer at
// the top.
if (isFirstInSequence) const SizedBox (height : 18 ) ,
if (username != null )
Padding (
padding : const EdgeInsets . only (
left : 13 ,
right : 13 ,
) ,
child : Text (
username ! ,
style : const TextStyle (
fontWeight : FontWeight . bold ,
color : Colors . black87 ,
) ,
) ,
) ,
// The "speech" box surrounding the message.
Container (
decoration : BoxDecoration (
color : isMe
? Colors . grey[ 300 ]
: theme . colorScheme . secondary . withAlpha ( 200 ) ,
// Only show the message bubble's "speaking edge" if first in
// the chain.
// Whether the "speaking edge" is on the left or right depends
// on whether or not the message bubble is the current user.
borderRadius : BorderRadius . only (
topLeft : ! isMe && isFirstInSequence
? Radius . zero
: const Radius . circular ( 12 ) ,
topRight : isMe && isFirstInSequence
? Radius . zero
: const Radius . circular ( 12 ) ,
bottomLeft : const Radius . circular ( 12 ) ,
bottomRight : const Radius . circular ( 12 ) ,
) ,
) ,
// Set some reasonable constraints on the width of the
// message bubble so it can adjust to the amount of text
// it should show.
constraints : const BoxConstraints (maxWidth : 200 ) ,
padding : const EdgeInsets . symmetric (
vertical : 10 ,
horizontal : 14 ,
) ,
// Margin around the bubble.
margin : const EdgeInsets . symmetric (
vertical : 4 ,
horizontal : 12 ,
) ,
child : Text (
message ,
style : TextStyle (
// Add a little line spacing to make the text look nicer
// when multilined.
height : 1.3 ,
color : isMe
? Colors . black87
: theme . colorScheme . onSecondary ,
) ,
softWrap : true ,
) ,
) ,
] ,
) ,
] ,
) ,
) ,
] ,
) ;
}
}
1) first와 next 값에 따라서, 유저네임, 프로필, 버블 모양, 위치 등의 모양이 바뀌게 설정 되었다. 하나씩 보면 충분히 이해 할 수 있음
-결과 화면