이번에는 채팅 화면에 실제 데이터를 매핑하고 동시 채팅을 실행해 보겠습니다.
개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1
화면 구현은 아래와 같습니다.
1. 내가 포함된 채팅방을 보여주는 화면 - 내가 팔거나 살려고 하는 채팅방 리스트 입니다. 게시글 제목과 마지막 메시지 등을 표시
2. 채팅방 리스트에서 클릭을 하면 해당 채팅방이 보여짐 - 상단의 아이템 정보를 실정보로 매핑(게시글제목, 금액, 가격제안 여부, 아이템 이미지 표시)
실제 메시지 구현 동영상입니다.
./src/repo/chat_service.dart - 내가 포함된 채팅방 조회 함수
Future<List<ChatroomModel2>> getMyChatList(String myUserKey) async {
List<ChatroomModel2> chatrooms = [];
// todo: I am as a buyer
QuerySnapshot<Map<String, dynamic>> buying = await FirebaseFirestore.instance
.collection(COL_CHATROOMS)
.where(DOC_BUYERKEY, isEqualTo: myUserKey)
.get();
// todo: I am as a seller
QuerySnapshot<Map<String, dynamic>> selling = await FirebaseFirestore.instance
.collection(COL_CHATROOMS)
.where(DOC_SELLERKEY, isEqualTo: myUserKey)
.get();
for (var documentSnapshot in buying.docs) {
ChatroomModel2 chatroom = ChatroomModel2.fromJson(documentSnapshot.data());
chatroom.chatroomKey = documentSnapshot.id;
chatroom.reference = documentSnapshot.reference;
chatrooms.add(chatroom);
}
for (var documentSnapshot in selling.docs) {
ChatroomModel2 chatroom = ChatroomModel2.fromJson(documentSnapshot.data());
chatroom.chatroomKey = documentSnapshot.id;
chatroom.reference = documentSnapshot.reference;
chatrooms.add(chatroom);
}
// 정렬하는 기능, 내림차순 정렬
chatrooms.sort((a, b) => (b.lastMsgTime).compareTo(a.lastMsgTime));
return chatrooms;
}
./src/models/chatroom_model.dart - negotiable 필드 추가, 상세 코드를 보려면 "더보기" 클릭하세요
class ChatroomModel2 {
String itemImage;
String itemTitle;
String itemKey;
String itemAddress;
num itemPrice;
String sellerKey;
String buyerKey;
String sellerImage;
String buyerImage;
GeoFirePoint geoFirePoint;
String lastMsg;
DateTime lastMsgTime;
String lastMsgUserKey;
String chatroomKey;
bool negotiable;
DocumentReference? reference;
ChatroomModel2({
required this.itemImage,
required this.itemTitle,
required this.itemKey,
required this.itemAddress,
required this.itemPrice,
required this.sellerKey,
required this.buyerKey,
required this.sellerImage,
required this.buyerImage,
required this.geoFirePoint,
this.lastMsg = '',
required this.lastMsgTime,
this.lastMsgUserKey = '',
required this.chatroomKey,
required this.negotiable,
this.reference,
});
factory ChatroomModel2.fromJson(Map<String, dynamic> json) => ChatroomModel2(
itemImage: json[DOC_ITEMIMAGE] ?? '',
itemTitle: json[DOC_ITEMTITLE] ?? '',
itemKey: json[DOC_ITEMKEY] ?? '',
itemAddress: json[DOC_ITEMADDRESS] ?? '',
itemPrice: json[DOC_ITEMPRICE] ?? 0,
sellerKey: json[DOC_SELLERKEY] ?? '',
buyerKey: json[DOC_BUYERKEY] ?? '',
sellerImage: json[DOC_SELLERIMAGE] ?? '',
buyerImage: json[DOC_BUYERIMAGE] ?? '',
geoFirePoint: json[DOC_GEOFIREPOINT] == null
? GeoFirePoint(0, 0)
: GeoFirePoint((json[DOC_GEOFIREPOINT][DOC_GEOPOINT]).latitude,
(json[DOC_GEOFIREPOINT][DOC_GEOPOINT]).longitude),
lastMsg: json[DOC_LASTMSG] ?? '',
lastMsgTime: json[DOC_LASTMSGTIME] == null
? DateTime.now().toUtc()
: (json[DOC_LASTMSGTIME] as Timestamp).toDate(),
lastMsgUserKey: json[DOC_LASTMSGUSERKEY] ?? '',
chatroomKey: json[DOC_CHATROOMKEY] ?? '',
negotiable: json[DOC_NEGOTIABLE] ?? false,
);
Map<String, dynamic> toJson() => {
DOC_ITEMIMAGE: itemImage,
DOC_ITEMTITLE: itemTitle,
DOC_ITEMKEY: itemKey,
DOC_ITEMADDRESS: itemAddress,
DOC_ITEMPRICE: itemPrice,
DOC_SELLERKEY: sellerKey,
DOC_BUYERKEY: buyerKey,
DOC_SELLERIMAGE: sellerImage,
DOC_BUYERIMAGE: buyerImage,
DOC_GEOFIREPOINT: geoFirePoint.data,
DOC_LASTMSG: lastMsg,
DOC_LASTMSGTIME: lastMsgTime,
DOC_LASTMSGUSERKEY: lastMsgUserKey,
DOC_CHATROOMKEY: chatroomKey,
DOC_NEGOTIABLE: negotiable,
};
static String generateChatRoomKey({required String buyer, required String itemKey}) {
return '${itemKey}_$buyer';
}
}
./src/screens/home/item_detail_page.dart - 모델링 변경으로 클래스 생성자 변경
ChatroomModel2 _chatroomModel = ChatroomModel2(
itemImage: itemModel.imageDownloadUrls[0],
itemTitle: itemModel.title,
itemKey: newItemKey,
itemAddress: itemModel.address,
itemPrice: itemModel.price,
sellerKey: itemModel.userKey,
buyerKey: userModel.userKey,
sellerImage: 'https://minimaltoolkit.com/images/randomdata/male/101.jpg',
buyerImage: 'https://minimaltoolkit.com/images/randomdata/female/41.jpg',
geoFirePoint: itemModel.geoFirePoint,
chatroomKey: chatroomKey,
lastMsgTime: DateTime.now().toUtc(),
negotiable: itemModel.negotiable, // <<== 새로 추가된 변수
);
./src/screens/chat/chat_list_page.dart - 내가 표함된 채팅방 리스트 및 화면 표시
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../constants/data_keys.dart';
import '../../models/chatroom_model.dart';
import '../../repo/chat_service.dart';
import '../../utils/logger.dart';
import '../../utils/time_calculation.dart';
class ChatListPage extends StatelessWidget {
final String userKey;
const ChatListPage({Key? key, required this.userKey}) : super(key: key);
@override
Widget build(BuildContext context) {
return FutureBuilder<List<ChatroomModel2>>(
future: ChatService().getMyChatList(userKey),
builder: (context, snapshot) {
Size _size = MediaQuery.of(context).size;
return Scaffold(
body: ListView.separated(
itemBuilder: (context, index) {
ChatroomModel2 chatroomModel = snapshot.data![index];
// 현재 시간과 작성시간의 차이 계산
String gapTime = TimeCalculation.getTimeDiff(chatroomModel.lastMsgTime);
// 화면 표시용 지역명 표시
List _address = chatroomModel.itemAddress.split(' ');
String _detail = _address[_address.length - 1];
String _location = '';
if (_detail.contains('(') && _detail.contains(')')) {
_location = _detail.replaceAll('(', '').replaceAll(')', '');
} else {
_location = _address[2];
}
return ListTile(
onTap: () {
// 클릭시 해당 채팅방으로 이동
logger.d(chatroomModel.chatroomKey);
Get.toNamed("/$ROUTE_ITEM_DETAIL/${chatroomModel.chatroomKey}");
},
// 작성자 이미지가 없으므로 임시 이미지로 표시
leading: ExtendedImage.network(
'https://randomuser.me/api/portraits/women/18.jpg',
height: _size.height / 8,
width: _size.width / 8,
fit: BoxFit.cover,
shape: BoxShape.circle,
),
// 제품 이미지 표시
trailing: ExtendedImage.network(
chatroomModel.itemImage,
height: _size.height / 8,
width: _size.width / 8,
fit: BoxFit.cover,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(4),
),
title: Row(
children: [
// 게시글 타이틀 표시
Expanded(
child: Text(
chatroomModel.itemTitle,
maxLines: 1,
// 1줄 이상이면 ... 으로 표시
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.subtitle1,
),
),
// 간략 주소 및 (현제일자-작성일자) 표시,
Text(
_location == '+'
? _address[0] + '/' + gapTime
: _location + '/' + gapTime,
//chatroomModel.itemAddress,
maxLines: 1,
// 1줄 이상이면 ... 으로 표시
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.subtitle2,
),
],
),
// 마지막 메시지 표시
subtitle: Text(
chatroomModel.lastMsg,
maxLines: 1,
// 1줄 이상이면 ... 으로 표시
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyText1,
),
);
},
separatorBuilder: (context, index) {
return Divider(thickness: 1, height: 1, color: Colors.grey[300]);
},
itemCount: snapshot.hasData ? snapshot.data!.length : 0),
);
});
}
}
./src/screens/main_screen.dart - 새로운 ChatListPage 페이지 추가
body: IndexedStack(
index: _bottomSelectedIndex,
children: <Widget>[
const ItemsScreen(),
(UserController.to.userModel.value == null)
? Container()
: MapScreen(UserController.to.userModel.value!),
(UserController.to.userModel.value == null)
? Container()
: GoogleMapScreen(UserController.to.userModel.value!),
// 새로운 화면(채팅 리스트 화면) 추가
(UserController.to.userModel.value == null)
? Container()
: ChatListPage(userKey: UserController.to.userModel.value!.userKey),
Container(color: Colors.accents[3]),
Container(color: Colors.accents[5]),
],
),
./src/screens/chat/chatroom_screen.dart - 새로운 방식으로 접근하기 위해서 채팅화면을 수정함
_chatroomModel 은 nullable 이므로 사용하기전에는 반드시 null 확인을 해야한다.
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_multi_formatter/flutter_multi_formatter.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:shimmer/shimmer.dart';
import '../../models/chat_model.dart';
import '../../models/chatroom_model.dart';
import '../../models/user_model.dart';
import '../../states/chat_controller.dart';
import '../../states/user_controller.dart';
import '../../utils/logger.dart';
import 'chat.dart';
class ChatroomScreen extends StatefulWidget {
const ChatroomScreen({Key? key}) : super(key: key);
@override
State<ChatroomScreen> createState() => _ChatroomScreenState();
}
class _ChatroomScreenState extends State<ChatroomScreen> {
late String chatroomKey;
final TextEditingController _textEditingController = TextEditingController();
late final ChatController chatController;
@override
void initState() {
// TODO: implement initState
chatroomKey = Get.parameters['chatroomKey']!;
chatController = Get.put(ChatController(chatroomKey));
logger.d(chatroomKey);
super.initState();
}
@override
Widget build(BuildContext context) {
// logger.d('${Get.parameters['chatroomKey']}');
Size _size = MediaQuery.of(context).size;
UserModel1 userModel = UserController.to.userModel.value!;
// ChatController 내부의 chatList, chatroomModel 변수를 접근하기 위해서,
return GetBuilder<ChatController>(
builder: (controller) {
return Scaffold(
appBar: AppBar(),
backgroundColor: Colors.grey[200],
// 화면 하단의 메뉴바 때문에 SafeArea 로 wrapping 해야 오동작을 방지함.
body: SafeArea(
child: Column(
children: [
// 게시글 정보를 간략히 표시
_buildItemInfo(context),
// 채팅 메시지 표시 부분
Expanded(
child: ListView.separated(
shrinkWrap: true,
// 최신 메시지가 아래에 위치하게 설정,
reverse: true,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
bool _isMine = controller.chatList[index].userKey == userModel.userKey;
return Chat(
size: _size,
isMine: _isMine,
chatModel: controller.chatList[index], //chatNotifier.chatList[index],
);
},
// 날짜가 다르면 디바이터 대신 일장 정보를 표시하게 함,
separatorBuilder: (context, index) {
if (DateFormat('yyyy-MM-dd').format(controller.chatList[index].createdDate) ==
DateFormat('yyyy-MM-dd')
.format(controller.chatList[index + 1].createdDate)) {
return const SizedBox(height: 12);
} else {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
DateFormat('yyyy-MM-dd')
.format(controller.chatList[index].createdDate),
),
),
);
}
},
itemCount: controller.chatList.length, //chatNotifier.chatList.length,
),
),
const Padding(padding: EdgeInsets.all(4)),
// 메시지 입력 창
_buildInputBar(userModel)
],
),
),
);
},
);
}
// 컬럼내부에 리스트타일(높이 관련 오류가 있어서 나중에 row+column 조함으로 변경함)과 버튼으로 구성 예정
MaterialBanner _buildItemInfo(BuildContext context) {
Rxn<ChatroomModel2> _chatroomModel = ChatController.to.chatroomModel;
return MaterialBanner(
// 공간 조절을 위해서 패딩을 수정함
padding: EdgeInsets.zero,
leadingPadding: EdgeInsets.zero,
// actions 는 빈칸
actions: [Container()],
// content 에만 위젯을 넣어서 표시 예정임.
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 16, right: 8, top: 8, bottom: 8),
child: _chatroomModel.value == null
? Shimmer.fromColors(
highlightColor: Colors.grey[200]!,
baseColor: Colors.grey,
child: Container(
width: 48,
height: 48,
color: Colors.white,
),
)
: ExtendedImage.network(
_chatroomModel.value!.itemImage,
width: 48,
height: 48,
fit: BoxFit.cover,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
// todo: 거래완료 여부 확인 필드 추가
text: TextSpan(
text: '거래완료',
style: Theme.of(context).textTheme.bodyText1,
children: [
TextSpan(
text: _chatroomModel.value == null
? ''
: ' ' + _chatroomModel.value!.itemTitle,
// text: ' 복숭아 떨이',
style: Theme.of(context).textTheme.bodyText2)
]),
),
RichText(
text: TextSpan(
text: _chatroomModel.value == null
? ''
: _chatroomModel.value!.itemPrice
.toCurrencyString(mantissaLength: 0, trailingSymbol: '원'),
style: Theme.of(context).textTheme.bodyText1,
children: [
TextSpan(
text: _chatroomModel.value == null
? ''
: _chatroomModel.value!.negotiable
? ' (가격제안가능)'
: ' (가격제안불가)',
style: _chatroomModel.value == null
? Theme.of(context)
.textTheme
.bodyText2!
.copyWith(color: Colors.black26)
: _chatroomModel.value!.negotiable
? Theme.of(context)
.textTheme
.bodyText2!
.copyWith(color: Colors.blue)
: Theme.of(context)
.textTheme
.bodyText2!
.copyWith(color: Colors.black26),
)
]),
),
],
),
],
),
Padding(
padding: const EdgeInsets.only(left: 16, bottom: 8),
// 버튼 사이즈를 줄이기 위해서 SizedBox 추가
child: SizedBox(
height: 32,
child: TextButton.icon(
onPressed: () {
debugPrint('후기남기기 클릭~~');
},
icon: const Icon(
Icons.edit,
// 버튼 사이즈를 줄이기 위해서 size 추가 설정,
size: 16,
color: Colors.black87,
),
label: Text(
'후기 남기기',
style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Colors.black87),
),
style: TextButton.styleFrom(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: BorderSide(color: Colors.grey[300]!, width: 2),
),
),
),
),
),
],
),
);
}
Widget _buildInputBar(UserModel1 userModel) {
return SizedBox(
height: 48,
child: Row(
children: [
IconButton(
onPressed: () {},
icon: const Icon(
Icons.add,
color: Colors.grey,
),
),
Expanded(
child: TextFormField(
controller: _textEditingController,
decoration: InputDecoration(
hintText: '메시지를 입력하세요',
// 메시지 입력박스를 조금 작게 줄임,
isDense: true,
// fillColor & filled 동시에 설정해야함.
fillColor: Colors.white,
filled: true,
suffixIcon: GestureDetector(
onTap: () {
logger.d('Icon clicked');
},
child: const Icon(
Icons.emoji_emotions_outlined,
color: Colors.grey,
),
),
// 아이콘때문에 입력 박스가 커져서 줄여줌 설정,
suffixIconConstraints: BoxConstraints.tight(const Size(40, 40)),
// 입력 박스의 패딩 공간을 줄임, 미설정시 16쯤 됨,
contentPadding: const EdgeInsets.all(10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(color: Colors.redAccent),
),
),
),
),
IconButton(
onPressed: () async {
ChatModel2 chatModel = ChatModel2(
userKey: userModel.userKey,
msg: _textEditingController.text,
createdDate: DateTime.now(), // addNewChat->toJson 에서 toUtc() 재처리함.
);
// await ChatService().createNewChat(chatroomKey, chatModel);
// Obs 처리하여 삭제시에도 바로 반영된다. 그래서 삭제시 화면에 바로 반영되서 기존것으로 원복함.
ChatController.to.addNewChat(chatModel);
// logger.d(_textEditingController.text.toString());
_textEditingController.clear();
},
icon: const Icon(
Icons.send,
color: Colors.grey,
),
)
],
),
);
}
}
'Flutter > 12 Clone 'Used Goods app'' 카테고리의 다른 글
[Flutter] Clone - 당근마켓52(Chat - 4) 메시지 스타일 (0) | 2022.09.07 |
---|---|
[Flutter] Clone - 당근마켓51(Chat - 3) 메시지 표시 (0) | 2022.09.06 |
[Flutter] Clone - 당근마켓50(Chat - 2) 상단/하단 정보 표시 (0) | 2022.09.02 |
[Flutter] Clone - 당근마켓49(Chat - 1) (1) | 2022.09.01 |
[Flutter] Clone - 당근마켓48(chatroomModel, chatModel) (0) | 2022.08.31 |