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

Flutter - 无法在flutter web中使用动态链接

无法使用 Flutter 1.22.3 编译 Flutter 应用程序