반응형
Flutter 플러터 파이어베이스 채팅 앱 구현하기 Firestore Database
main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'chat_screen.dart';
import 'random_chat_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: AuthenticationScreen(),
);
}
}
class AuthenticationScreen extends StatelessWidget {
final FirebaseAuth _auth = FirebaseAuth.instance;
Future<void> _signInAnonymously(BuildContext context) async {
try {
UserCredential userCredential = await _auth.signInAnonymously();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => MainScreen(userId: userCredential.user!.uid),
),
);
} catch (e) {
print("로그인 오류: $e");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('랜덤 채팅 로그인')),
body: Center(
child: ElevatedButton(
onPressed: () => _signInAnonymously(context),
child: Text('로그인'),
),
),
);
}
}
class MainScreen extends StatefulWidget {
final String userId;
MainScreen({required this.userId});
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
bool isLoading = false;
StreamSubscription<String?>? _matchSubscription;
Timer? _countdownTimer;
int countdownSeconds = 10;
Future<void> _startRandomChat() async {
setState(() {
isLoading = true;
});
// 사용자를 대기열에 추가
await RandomChatService().addToQueue(widget.userId);
// 대기 중 모달 창 띄우기
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) {
// 타이머를 시작하고, 타이머가 실행될 때마다 setDialogState를 호출하여 UI 갱신
if (_countdownTimer == null || !_countdownTimer!.isActive) {
_countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (countdownSeconds > 0) {
setState(() {
countdownSeconds--;
});
setDialogState(() {}); // 모달 창 UI 갱신
} else {
_cancelMatchmaking();
if (Navigator.canPop(context)) {
Navigator.pop(context); // 대기 중 모달 창 닫기
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("매칭 시간이 초과되어 취소되었습니다.")),
);
}
});
}
return AlertDialog(
title: Text('상대방을 찾고 있습니다...'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text("대기 시간 : $countdownSeconds초"),
],
),
);
},
),
);
// 상대방을 기다리며 매칭을 확인
_matchSubscription = RandomChatService().waitForMatch(widget.userId).listen((chatId) {
if (chatId != null) {
_cancelMatchmaking(); // 타이머와 매칭 구독 해제
Navigator.pop(context); // 대기 중 모달 창 닫기
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(chatId: chatId, userId: widget.userId),
),
);
setState(() {
isLoading = false;
});
}
});
}
void _startCountdownTimer() {
_countdownTimer = Timer.periodic(Duration(seconds: 1), (timer) {
if (countdownSeconds > 0) {
setState(() {
countdownSeconds--;
});
} else {
_cancelMatchmaking();
if (Navigator.canPop(context)) {
Navigator.pop(context); // 대기 중 모달 창 닫기
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("매칭 시간이 초과되어 취소되었습니다.")),
);
}
});
}
void _cancelMatchmaking() {
_matchSubscription?.cancel();
_countdownTimer?.cancel();
_countdownTimer = null; // 타이머 해제 후 null로 초기화하여 재실행 방지
setState(() {
isLoading = false;
countdownSeconds = 10;
});
}
@override
void dispose() {
_cancelMatchmaking(); // 모든 타이머와 스트림 구독 해제
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('메인 화면')),
body: Center(
child: isLoading
? CircularProgressIndicator()
: ElevatedButton(
onPressed: _startRandomChat,
child: Text('랜덤 채팅 시작'),
),
),
);
}
}
chat_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class ChatScreen extends StatefulWidget {
final String chatId;
final String userId;
ChatScreen({required this.chatId, required this.userId});
@override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final TextEditingController messageController = TextEditingController();
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_sendSystemMessage("${widget.userId} 님이 입장하였습니다.");
}
@override
void dispose() {
_sendSystemMessage("${widget.userId} 님이 방에서 나갔습니다.");
_scrollController.dispose();
super.dispose();
}
Future<void> _sendSystemMessage(String message) async {
await _firestore.collection('chats').doc(widget.chatId).collection('messages').add({
'text': message,
'createdAt': FieldValue.serverTimestamp(),
'isSystemMessage': true,
'senderId': 'system', // 시스템 메시지는 senderId를 'system'으로 설정
});
}
Future<void> _sendMessage() async {
if (messageController.text.trim().isNotEmpty) {
await _firestore.collection('chats').doc(widget.chatId).collection('messages').add({
'senderId': widget.userId,
'text': messageController.text,
'createdAt': FieldValue.serverTimestamp(),
'isSystemMessage': false,
});
messageController.clear();
_scrollToBottom();
}
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('랜덤 채팅'),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
),
body: Column(
children: [
Expanded(
child: StreamBuilder(
stream: _firestore
.collection('chats')
.doc(widget.chatId)
.collection('messages')
.orderBy('createdAt')
.snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
// 새 메시지가 수신되면 자동 스크롤
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
return ListView(
controller: _scrollController,
children: snapshot.data!.docs.map((doc) {
final data = doc.data() as Map<String, dynamic>;
bool isSystemMessage = data['isSystemMessage'] ?? false;
String senderId = data['senderId'] ?? '';
if (isSystemMessage) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
data['text'],
style: TextStyle(
color: Colors.grey,
fontStyle: FontStyle.italic,
),
),
),
);
} else {
bool isCurrentUser = senderId == widget.userId;
return Align(
alignment: isCurrentUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: isCurrentUser ? Colors.blue[100] : Colors.grey[300],
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
bottomLeft: isCurrentUser ? Radius.circular(12) : Radius.circular(0),
bottomRight: isCurrentUser ? Radius.circular(0) : Radius.circular(12),
),
),
child: Text(
data['text'],
style: TextStyle(
color: Colors.black,
),
),
),
);
}
}).toList(),
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: messageController,
decoration: InputDecoration(hintText: '메시지를 입력하세요'),
onSubmitted: (value) => _sendMessage(),
),
),
IconButton(
icon: Icon(Icons.send),
onPressed: _sendMessage,
),
],
),
),
],
),
);
}
}
random_chat_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
class RandomChatService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Future<void> addToQueue(String userId) async {
await _firestore.collection('queue').doc(userId).set({
'userId': userId,
'createdAt': FieldValue.serverTimestamp(),
});
}
Stream<String?> waitForMatch(String userId) async* {
await for (var snapshot in _firestore
.collection('queue')
.where('userId', isNotEqualTo: userId)
.snapshots()) {
if (snapshot.docs.isNotEmpty) {
final otherUserId = snapshot.docs.first.id;
// 두 사용자 ID를 이용해 기존 채팅방이 있는지 확인하고 가져오기
final chatId = await _findOrCreateChatRoom(userId, otherUserId);
yield chatId;
} else {
yield null;
}
}
}
Future<String> _findOrCreateChatRoom(String userId, String otherUserId) async {
// 두 사용자가 이미 존재하는 채팅방이 있는지 확인
final existingChat = await _firestore
.collection('chats')
.where('users', arrayContains: userId)
.get();
for (var doc in existingChat.docs) {
final users = List<String>.from(doc['users']);
if (users.contains(otherUserId)) {
return doc.id; // 기존 채팅방 ID 반환
}
}
// 기존 채팅방이 없다면 새로 생성
var chatDoc = await _firestore.collection('chats').add({
'users': [userId, otherUserId],
'createdAt': FieldValue.serverTimestamp(),
});
// 대기열에서 두 사용자 제거
await _firestore.collection('queue').doc(userId).delete();
await _firestore.collection('queue').doc(otherUserId).delete();
return chatDoc.id;
}
}
반응형