通过这篇文章,我想分享如何借助 Firebase 在 flutter 中搭建基本的实时聊天。本文没有深入探讨这个问题,而是专注于如何构建聊天的基本逻辑。
我们可以假设 FirebaseAuth 用于登录,并因此保存当前登录用户的 uid。
firebase_auth: null
firebase_core: ^2.25.4
firebase_database: null
firebase_messaging: null
firebase_storage: ^11.6.6
final user = FirebaseAuth.instance.currentUser;
我使用了 firebase 实时数据库来实时存储和获取信息。
final DatabaseReference databaseReference = FirebaseDatabase.instance.ref();
final DatabaseReference chatRef = FirebaseDatabase.instance.reference().child('community');
从 UI 开始,然后回到数据处理部分。
Flutter Chat Bubble/UI
flutter_chat_bubble 用于构建聊天 UI :
flutter_chat_bubble:^ 2.0 .2
ChatBubble(
clipper: ChatBubbleClipper1(type: BubbleType.sendBubble),
alignment: Alignment.topRight,
margin: EdgeInsets.only(top: 20),
backGroundColor: Colors.blue,
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
style: TextStyle(color: Colors.white),
),
),
)
使用 ChatBubble 部件可以帮助我们以简单方便的方式构建发送和接收消息的用户界面。
用户界面是使用流生成器构建的,它可以监听来自 firebase 实时数据库的值变化。
数据以 Map<String,dynamic> 的形式发送,如下所示:
Map<String,dynamic> data = {
"senderId" : user?.uid, //id of current logged in user
"avatar" : user?.photoURL,
"content_type" : "text",
"content" : _content.text.trim(),
"dateTime" : DateTime.now().toIso8601String()
};
dateTime 将用于对消息进行排序,并在用户界面中反映消息的日期和时间。
senderId 将用于区分发送和接收消息。此时,我们假设 FirebaseAuth 拥有当前登录的用户 ID,并将此 ID 与发件人 ID 进行交叉检查。如果匹配,则发送消息(右对齐),否则接收消息(左对齐)。
现在,开始构建聊天 UI。
String? previousDate; //to display dates of messages
late ScrollController _scrollController; //to auto scroll to last message
//to initiate auto scroll at first
@override
void initState() {
super.initState();
_scrollController = ScrollController();
Future.delayed(const Duration(milliseconds: 600), () {
_scrollToBottom();
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 600),
curve: Curves.easeOut,
);
}
});
}
StreamBuilder(
stream: chatRef.onValue,
builder: (context, AsyncSnapshot<DatabaseEvent> snapshot) {
if (snapshot.hasData && snapshot.data!.snapshot.value != null) {
Map<dynamic, dynamic> map = snapshot.data!.snapshot.value as Map<dynamic, dynamic>;
List<dynamic> list = map.values.toList();
for (var chat in list) {
if (chat['dateTime'] is String) {
chat['dateTime'] = DateTime.parse(chat['dateTime']);
}
}
list.sort((a, b) => b['dateTime'].compareTo(a['dateTime']));
list = list.reversed.toList();
return ListView.builder(
controller: _scrollController,
itemCount: snapshot.data!.snapshot.children.length,
itemBuilder: (context, index) {
String messageDate = list[index]['dateTime'].toString().substring(0, 10);
bool displayDate = false;
if (previousDate == null || messageDate != previousDate) {
displayDate = true;
previousDate = messageDate;
}
return (list[index]['senderId'] == user!.uid)
? //send bubble
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (displayDate)
Center(
child: Text(
messageDate == currentDateString ? "Today" : messageDate.substring(0, 10),
style: DateTextStyle(),
),
),
ChatBubble(
clipper: ChatBubbleClipper1(type: BubbleType.sendBubble),
alignment: Alignment.topRight,
margin: const EdgeInsets.only(top: 20),
backGroundColor: Colors.blue,
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: (list[index]['content_type'] == "text")
? Text(
list[index]['content'],
style: const TextStyle(color: Colors.white, fontSize: 18),
)
: Container(
height: 100,
width: 150,
decoration: BoxDecoration(
color: Colors.white,
image: DecorationImage(
image: NetworkImage(list[index]['content']),
),
),
),
),
const SizedBox(width: 10),
CircleAvatar(
backgroundImage: NetworkImage(list[index]['avatar']),
radius: 10,
),
],
),
),
),
const SizedBox(height: 5,),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(list[index]['dateTime'].toString().substring(10,16),style: TimeTextStyle(),),
const SizedBox(width: 20,)
],
),
],
)
: //receive bubble
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (displayDate)
Center(
child: Text(
messageDate == currentDateString ? "Today" : messageDate.substring(0, 10),
style: DateTextStyle(),
),
),
ChatBubble(
clipper: ChatBubbleClipper1(type: BubbleType.receiverBubble),
backGroundColor: const Color(0xffE7E7ED),
margin: const EdgeInsets.only(top: 20),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
backgroundImage: NetworkImage(list[index]['avatar']),
radius: 10,
),
const SizedBox(width: 10),
Flexible(
child: (list[index]['content_type'] == "text")
? Text(
list[index]['content'],
style: const TextStyle(
color: Color.fromARGB(255, 3, 3, 3),
fontSize: 18,
),
)
: Container(
height: 100,
width: 150,
decoration: BoxDecoration(
color: Colors.white,
image: DecorationImage(
image: NetworkImage(list[index]['content']),
),
),
),
),
],
),
),
),
const SizedBox(height: 5,),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(width: 20,),
Text(list[index]['dateTime'].toString().substring(10,16),style: TimeTextStyle(),),
],
),
],
);
},
);
} else if (snapshot.hasError) {
return const Icon(Icons.error);
} else {
return Center(
child: Text(
"Chat responsibly ",
style: GoogleFonts.orbitron(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.grey
),
),
);
}
},
)
stream: chatRef.onValue,
生成器会检查 chatRef(实时数据库实例)的变化。每次值发生变化时,流构建器都会监听并重建 UI。
builder: (context, AsyncSnapshot<DatabaseEvent> snapshot) {
if (snapshot.hasData && snapshot.data!.snapshot.value != null) {
Map<dynamic, dynamic> map = snapshot.data!.snapshot.value as Map<dynamic, dynamic>;
List<dynamic> list = map.values.toList();
for (var chat in list) {
if (chat['dateTime'] is String) {
chat['dateTime'] = DateTime.parse(chat['dateTime']);
}
}
list.sort((a, b) => b['dateTime'].compareTo(a['dateTime']));
list = list.reversed.toList();
}
}
如果快照中有数据,则将其提取出来并转换成映射。for in 循环用于遍历列表中的每条信息。消息根据日期时间排序。
现在,流构建器返回一个 ListView.builder(),它有助于使用 ChatBubble 部件构建聊天 UI。
return ListView.builder(
controller: _scrollController,
itemCount: snapshot.data!.snapshot.children.length,
itemBuilder: (context, index) {
String messageDate = list[index]['dateTime'].toString().substring(0, 10);
bool displayDate = false;
if (previousDate == null || messageDate != previousDate) {
displayDate = true;
previousDate = messageDate;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
displayDate
? Text(
messageDate,
style: TextStyle(
color: Colors.grey,
fontSize: 12,
),
)
: SizedBox(height: 0),
(list[index]['senderId'] == user!.uid)
?
// Sender's message bubble widget
:
// Receiver's message bubble widget
],
);
},
);
在 ListView.builder() 中,我们会比较每条信息的上一日期和当前信息的日期。如果不同,则将 displayDate 设为 true。如果为 true,则显示信息 id 的日期。
然后,我们交叉检查来自 FirebaseAuth 的发件人 ID 和 uid。如果两者匹配,则通过发送气泡,否则通过接收气泡。
发送气泡和接收气泡用一列包装,每条信息的时间都会显示出来。
可使用灵活的部件来避免信息长度的叠加问题。行部件也可用于对齐目的。
两者都在下面提供:
// Send bubble
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (displayDate)
Center(
child: Text(
messageDate == currentDateString ? "Today" : messageDate.substring(0, 10),
style: DateTextStyle(),
),
),
ChatBubble(
clipper: ChatBubbleClipper1(type: BubbleType.sendBubble),
alignment: Alignment.topRight,
margin: const EdgeInsets.only(top: 20),
backGroundColor: Colors.blue,
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: (list[index]['content_type'] == "text")
? Text(
list[index]['content'],
style: const TextStyle(color: Colors.white, fontSize: 18),
)
: Container(
height: 100,
width: 150,
decoration: BoxDecoration(
color: Colors.white,
image: DecorationImage(
image: NetworkImage(list[index]['content']),
),
),
),
),
const SizedBox(width: 10),
CircleAvatar(
backgroundImage: NetworkImage(list[index]['avatar']),
radius: 10,
),
],
),
),
),
const SizedBox(height: 5,),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(list[index]['dateTime'].toString().substring(10,16),style: TimeTextStyle(),),
const SizedBox(width: 20,)
],
),
],
)
// Receive bubble
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (displayDate)
Center(
child: Text(
messageDate == currentDateString ? "Today" : messageDate.substring(0, 10),
style: DateTextStyle(),
),
),
ChatBubble(
clipper: ChatBubbleClipper1(type: BubbleType.receiverBubble),
backGroundColor: const Color(0xffE7E7ED),
margin: const EdgeInsets.only(top: 20),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
backgroundImage: NetworkImage(list[index]['avatar']),
radius: 10,
),
const SizedBox(width: 10),
Flexible(
child: (list[index]['content_type'] == "text")
? Text(
list[index]['content'],
style: const TextStyle(
color: Color.fromARGB(255, 3, 3, 3),
fontSize: 18,
),
)
: Container(
height: 100,
width: 150,
decoration: BoxDecoration(
color: Colors.white,
image: DecorationImage(
image: NetworkImage(list[index]['content']),
),
),
),
),
],
),
),
),
const SizedBox(height: 5,),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(width: 20,),
Text(list[index]['dateTime'].toString().substring(10,16),style: TimeTextStyle(),),
],
),
],
);
现在,在每条消息中,我们必须查找消息类型(文本、媒体……)并进行相应构建。如果内容类型是 text ,我就传递了一个 Text() 小部件,否则就传递一个显示媒体的容器。
具体代码如下所示:
Flexible(
child: (list[index]['content_type'] == "text")
? Text(
list[index]['content'],
style: const TextStyle(
color: Color.fromARGB(255, 3, 3, 3),
fontSize: 18,
),
)
: Container(
height: 100,
width: 150,
decoration: BoxDecoration(
color: Colors.white,
image: DecorationImage(
image: NetworkImage(list[index]['content']),
),
),
),
),
每条消息都有一个 Text() 小部件,用于显示消息的时间。
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(width: 20,),
Text(list[index]['dateTime'].toString().substring(10,16),style: TimeTextStyle(),),
],
),
现在,看看如何发送信息。
IconButton(
onPressed: () {
Map<String, dynamic> data = {
"senderId": user?.uid,
"avatar": user?.photoURL,
"content_type": "text",
"content": _content.text.trim(),
"dateTime": DateTime.now().toIso8601String()
};
sendMessage(context, data);
},
icon: const Icon(Icons.send_rounded),
),
void sendMessage(BuildContext context,Map<String,dynamic> data) {
databaseReference.child('community').push().set(data).whenComplete((){
_content.clear();
}) ;
}
上面显示的聊天是我的社区项目的一部分,GitHub repo 的链接 :
https://github.com/Alto-b/CodeFascia/blob/main/lib%2Fpresentation%2Fscreens%2Fcommunity%2Fcommunity_chat.dart
作者:Alto b
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/46721.html