Flutter - 使用 StreamBuilder 构建的聊天屏幕,多次显示消息
Posted
技术标签:
【中文标题】Flutter - 使用 StreamBuilder 构建的聊天屏幕,多次显示消息【英文标题】:Flutter - Chat Screen built with a StreamBuilder showing messages multiple times 【发布时间】:2021-09-28 00:31:55 【问题描述】:我在这个聊天屏幕上苦苦挣扎。该应用程序旨在提出问题(不是以下代码的一部分),用户可以选择答案或键入答案。当用户键入第一个答案时,一切都按计划进行,并显示第一条消息。然而,应用程序会继续显示第二个答案两次,第三个答案三次,依此类推。
我已经面临这个问题几天了,我无法弄清楚为什么应用程序会这样运行。您能否看一下代码并提出解决此问题的方法?
为了给你一些背景信息,这个聊天屏幕是一个更大的应用程序的一部分。当用户打开应用程序时,它应该订阅一个流。然后将每条消息推送到流中,无论是机器人提出的问题还是用户给出的答案。每次流广播某些内容(在我们的例子中是最新的用户输入)时,系统都会监听流并显示一条新消息。
我正在使用从流中构建的消息模型列表来显示消息。为了提出这个问题,我将模型简化到了极致,但实际上它有 23 个字段。创建此消息列表是我设法想到的最佳解决方案,但可能有更好的方法来处理这种情况。如果您知道,请随时告诉我。
这是我正在运行的代码。
import 'package:flutter/material.dart';
import 'dart:async';
StreamController<ChatMessageModel> _chatMessagesStreamController = StreamController<ChatMessageModel>.broadcast();
Stream _chatMessagesStream = _chatMessagesStreamController.stream;
const Color primaryColor = Color(0xff6DA7B9);
const Color secondaryColor = Color(0xffF0F0F0);
void main()
runApp(MyApp());
class MyApp extends StatelessWidget
@override
Widget build(BuildContext context)
return MaterialApp(
title: 'Chat Screen',
home: ChatScreen(),
);
class ChatMessageModel
final String message;
const ChatMessageModel(
this.message,
);
factory ChatMessageModel.turnSnapshotIntoListRecord(Map data)
return ChatMessageModel(
message: data['message'],
);
@override
List<Object> get props => [
message,
];
class ChatScreen extends StatefulWidget
static const String id = 'chat_screen9';
@override
_ChatScreenState createState() => _ChatScreenState();
class _ChatScreenState extends State<ChatScreen>
final _messageTextController = TextEditingController();
String _userInput;
@override
Widget build(BuildContext context)
return Scaffold(
backgroundColor: secondaryColor,
appBar: AppBar(
title: Row(
children: [
Container(
padding: EdgeInsets.all(8.0),
child: Text('Chat Screen',
style: TextStyle(color: Colors.white,),
),
)
],
),
backgroundColor: primaryColor,
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
MessagesStream(),
Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: primaryColor,
width: 1.0,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextField(
controller: _messageTextController,
onChanged: (value)
_userInput = value;
,
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0),
hintText: 'Type your answer here',
// border: InputBorder.none,
),
),
),
TextButton(
onPressed: ()
_messageTextController.clear();
debugPrint('Adding a ChatMessageModel with the message $_userInput to the Stream');
ChatMessageModel chatMessageModelRecord = ChatMessageModel(message: _userInput);
_chatMessagesStreamController.add(chatMessageModelRecord,);
,
child: Text(
'OK',
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
),
],
),
),
],
),
),
);
class MessagesStream extends StatelessWidget
List<ChatMessageModel> _allMessagesContainedInTheStream = [];
@override
Widget build(BuildContext context)
return StreamBuilder<ChatMessageModel>(
stream: _chatMessagesStream,
builder: (context, snapshot)
_chatMessagesStream.listen((streamedMessages)
// _allMessagesContainedInTheStream.clear();
debugPrint('Value from controller: $streamedMessages');
_allMessagesContainedInTheStream.add(streamedMessages);
);
return Expanded(
child: ListView.builder(
// reverse: true,
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
itemCount: _allMessagesContainedInTheStream.length,
itemBuilder: (BuildContext context, int index)
if (snapshot.hasData)
return UserChatBubble(chatMessageModelRecord: _allMessagesContainedInTheStream[index]);
,
),
);
,
);
class UserChatBubble extends StatelessWidget
final ChatMessageModel chatMessageModelRecord;
const UserChatBubble(
Key key,
@required this.chatMessageModelRecord,
) : super(key: key);
@override
Widget build(BuildContext context)
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 5,),
child: Container(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 7 / 10,),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15.0),
bottomRight: Radius.circular(15.0),
topLeft: Radius.circular(15.0),
),
color: primaryColor,
),
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 20,),
child: Text(chatMessageModelRecord.message,
style: TextStyle(
fontSize: 17,
// fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
],
);
【问题讨论】:
【参考方案1】:首先,感谢您提供有趣的问题和功能示例。我必须做一些小改动才能将其转换为“null-safety”,但我的代码也应该可以在您的计算机上运行。
您在初始化 _chatMessagesStream 侦听器时遇到的唯一问题。您应该只调用一次,最好在initState 中调用一次。
所以这里是你的解决方法:
class MessagesStream extends StatefulWidget
@override
_MessagesStreamState createState() => _MessagesStreamState();
class _MessagesStreamState extends State<MessagesStream>
final List<ChatMessageModel> _allMessagesContainedInTheStream = [];
@override
void initState()
_chatMessagesStream.listen((streamedMessages)
// _allMessagesContainedInTheStream.clear();
debugPrint('Value from controller: $streamedMessages');
_allMessagesContainedInTheStream.add(streamedMessages);
);
super.initState();
@override
Widget build(BuildContext context)
return StreamBuilder<ChatMessageModel>(
stream: _chatMessagesStream,
builder: (context, snapshot)
return Expanded(
child: ListView.builder(
// reverse: true,
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
itemCount: _allMessagesContainedInTheStream.length,
itemBuilder: (BuildContext context, int index)
if (snapshot.hasData)
return UserChatBubble(
chatMessageModelRecord:
_allMessagesContainedInTheStream[index],
);
else
print(snapshot.connectionState);
return Container();
,
),
);
,
);
还提供完整的 null 安全代码以防万一!
import 'package:flutter/material.dart';
import 'dart:async';
final StreamController<ChatMessageModel> _chatMessagesStreamController =
StreamController<ChatMessageModel>.broadcast();
final Stream<ChatMessageModel> _chatMessagesStream =
_chatMessagesStreamController.stream;
const Color primaryColor = Color(0xff6DA7B9);
const Color secondaryColor = Color(0xffF0F0F0);
void main()
runApp(MyApp());
class MyApp extends StatelessWidget
@override
Widget build(BuildContext context)
return MaterialApp(
title: 'Chat Screen',
home: ChatScreen(),
);
class ChatMessageModel
final String? message;
const ChatMessageModel(
this.message,
);
factory ChatMessageModel.turnSnapshotIntoListRecord(Map data)
return ChatMessageModel(
message: data['message'],
);
List<Object> get props => [
message!,
];
class ChatScreen extends StatefulWidget
static const String id = 'chat_screen9';
@override
_ChatScreenState createState() => _ChatScreenState();
class _ChatScreenState extends State<ChatScreen>
final _messageTextController = TextEditingController();
String? _userInput;
@override
Widget build(BuildContext context)
return Scaffold(
backgroundColor: secondaryColor,
appBar: AppBar(
title: Row(
children: [
Container(
padding: EdgeInsets.all(8.0),
child: Text(
'Chat Screen',
style: TextStyle(
color: Colors.white,
),
),
)
],
),
backgroundColor: primaryColor,
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
MessagesStream(),
Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: primaryColor,
width: 1.0,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextField(
controller: _messageTextController,
onChanged: (value)
_userInput = value;
,
decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(
vertical: 10.0, horizontal: 20.0),
hintText: 'Type your answer here',
// border: InputBorder.none,
),
),
),
TextButton(
onPressed: ()
_messageTextController.clear();
debugPrint(
'Adding a ChatMessageModel with the message $_userInput to the Stream');
ChatMessageModel chatMessageModelRecord =
ChatMessageModel(message: _userInput);
_chatMessagesStreamController.add(
chatMessageModelRecord,
);
,
child: Text(
'OK',
style: TextStyle(
color: primaryColor,
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
),
],
),
),
],
),
),
);
class MessagesStream extends StatefulWidget
@override
_MessagesStreamState createState() => _MessagesStreamState();
class _MessagesStreamState extends State<MessagesStream>
final List<ChatMessageModel> _allMessagesContainedInTheStream = [];
@override
void initState()
_chatMessagesStream.listen((streamedMessages)
// _allMessagesContainedInTheStream.clear();
debugPrint('Value from controller: $streamedMessages');
_allMessagesContainedInTheStream.add(streamedMessages);
);
super.initState();
@override
Widget build(BuildContext context)
return StreamBuilder<ChatMessageModel>(
stream: _chatMessagesStream,
builder: (context, snapshot)
return Expanded(
child: ListView.builder(
// reverse: true,
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0),
itemCount: _allMessagesContainedInTheStream.length,
itemBuilder: (BuildContext context, int index)
if (snapshot.hasData)
return UserChatBubble(
chatMessageModelRecord:
_allMessagesContainedInTheStream[index],
);
else
print(snapshot.connectionState);
return Container();
,
),
);
,
);
class UserChatBubble extends StatelessWidget
final ChatMessageModel chatMessageModelRecord;
const UserChatBubble(
Key? key,
required this.chatMessageModelRecord,
) : super(key: key);
@override
Widget build(BuildContext context)
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.symmetric(
vertical: 5,
horizontal: 5,
),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 7 / 10,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15.0),
bottomRight: Radius.circular(15.0),
topLeft: Radius.circular(15.0),
),
color: primaryColor,
),
padding: EdgeInsets.symmetric(
vertical: 8,
horizontal: 20,
),
child: Text(
"$chatMessageModelRecord.message",
style: TextStyle(
fontSize: 17,
// fontWeight: FontWeight.w500,
color: Colors.white,
),
),
),
),
],
);
【讨论】:
感谢您的快速回答和解释。主要的收获是在错误的地方订阅了流,这样做实际上是在每次发生新事件时重新订阅。我完全错过了这一点,但事后看来这是有道理的。谢谢,这很有帮助。也感谢您指出 null 安全问题以及如何解决它。以上是关于Flutter - 使用 StreamBuilder 构建的聊天屏幕,多次显示消息的主要内容,如果未能解决你的问题,请参考以下文章
Flutter屏幕像素适配方案 ( flutter_screenutil 插件 )
Flutter - 使用 google_sign_in 库时未找到 <Flutter/Flutter.h>
Flutter——如何使用 html 链接渲染 Flutter 文本 [重复]
flutter系列之:在flutter中使用导航Navigator