前言
我们在前面花了很大篇幅介绍 Provider
状态管理,这是因为在 Flutter 中,Provider
是众多状态管理插件的首选。本篇以即时聊天为例,来讲述 Provider
的综合应用,也算是 Provider
状态管理系列的终结篇。本篇涉及的内容如下:
联系人界面构建
我们在聊天前,需要选择对应的联系人进行单聊,因此需要构建一个联系人列表。这里我们使用简单的 ListView.builder+Mock 数据构建联系人列表。界面如下所示,其中关键的就是点击联系人时将联系人的 id 通过路由传递过去,以便发送消息时通过用户 id 指定接收用户。
return ListTile(
leading: _getRoundImage(contactors[index].avatar, 50),
title: Text(contactors[index].nickname),
subtitle: Text(
contactors[index].description,
style: TextStyle(fontSize: 14.0, color: Colors.grey),
),
onTap: () {
debugPrint(contactors[index].id);
RouterManager.router.navigateTo(context,
'${RouterManager.chatPath}?toUserId=${contactors[index].id}');
},
);
复制代码
聊天界面的实现
我们将发送的消息放在右边,将接收到的消息放在左边,距做还是居右通过 Container
的 margin
来实现。至于区分,通过消息对象的fromUserId
来区分,如果 fromUserId
和当前用户id
一致,则是发送出去的消息,否则就是接收到的消息。在这里我们因为还没有用户体系,先将当前的用户 id
写死。为了实现模拟器之间的聊天,我们一个模拟器设置为 user1
,一个设置为 user2
。界面也是使用ListView.builder
(万能不?)构建。
return ListView.builder(
itemBuilder: (context, index) {
MessageEntity message = messages[index];
double margin = 20;
double marginLeft = message.fromUserId == 'user1' ? 60 : margin;
double marginRight = message.fromUserId == 'user1' ? margin : 60;
return Container(
margin: EdgeInsets.fromLTRB(marginLeft, margin, marginRight, margin),
padding: EdgeInsets.all(15),
alignment: message.fromUserId == 'user1'
? Alignment.centerRight
: Alignment.centerLeft,
decoration: BoxDecoration(
color: message.fromUserId == 'user1'
? Colors.green[300]
: Colors.blue[400],
borderRadius: BorderRadius.circular(10)),
child: Text(
message.content,
style: TextStyle(color: Colors.white),
),
);
},
itemCount: messages.length,
);
复制代码
聊天界面的一个特点是会接收StreamProvider
推送的最新的消息,为了统一,我们将接收消息和发送消息都通过StreamProvider
推送更新界面。
// 发送消息时将消息加入到流控制器中
void sendMessage(String event, T message) {
_socket.emit(event, message);
_socketResponse.sink.add(message);
}
// 接收消息时也加入到流控制器中
_socket.on(recvEvent, (data) {
_socketResponse.sink.add(data);
});
复制代码
这样不管是接收消息还是发送消息都会通过 StreamProvider
重新构建聊天界面。那问题来了,聊天列表数据如何刷新呢?
消息界面的 MultiProvider
消息界面需要接收 StreamProvider
的消息流,还需要使用消息列表数据,这里我们使用了 MultiProvider
。其中消息发送框和聊天界面共用 ChatMessageModel
(仅为演示,实际可以拆分开)。
final chatMessageModel = ChatMessageModel();
//...
body: Stack(
alignment: Alignment.bottomCenter,
children: [
MultiProvider(
providers: [
StreamProvider<Map<String, dynamic>?>(
create: (context) => streamSocket.getResponse,
initialData: null),
ChangeNotifierProvider.value(value: chatMessageModel)
],
child: StreamDemo(),
),
ChangeNotifierProvider.value(
child: MessageReplyBar(messageSendHandler: (message) {
Map<String, String> json = {
'fromUserId': 'user1',
'toUserId': widget.toUserId,
'contentType': 'text',
'content': message
};
streamSocket.sendMessage('chat', json);
}),
value: chatMessageModel,
),
]
//...
复制代码
其中ChatMessageModel
即消息列表状态数据,里面只有一个消息对象数组和一个添加消息方法,以及一个 content
属性是给消息回复框使用的。
这里就有一个问题,StreamProvider
推送 StreamSocket
过来的消息的时候, ChatMessageModel
其实是不知道的。如果要知道,一个办法就是在 StreamSocket
引用 ChatMessageModel
对象,然后调用其 addMessage
方法添加消息。但是这样会增加两个类的耦合。还有一种方式是取巧的方式了,那就是 StreamdDemo
的 build
方法能够获取到 StreamSocket
推送的最新消息,在这里读取到最新的消息后就可以添加到消息列表了。由于前面我们发送消息和接收消息都将消息加入到了消息流中,这样处理方式就统一了。
这种方式需要注意,Provider
不允许在组件的build
方法中再次调用类似 notifyListeners
的方法通知该组件刷新,因此在 ChatMessageModel
的 addMessage
方法里不可以使用notifyListeners
来通知组件刷新,否则会出现同一组件刷新冲突。实际上,因为另一个Provider
已经通知该组件刷新了,因此也没必要再通知了。当然,这仅仅是一种取巧方法,假设这个addMessage
方法还需要通知其他组件刷新,那这种形式就就不可取了。
class ChatMessageModel with ChangeNotifier {
List<MessageEntity> _messages = [];
List<MessageEntity> get messages => _messages;
String content = '';
void addMessage(Map<String, dynamic> json) {
_messages.add(MessageEntity.fromJson(json));
}
}
复制代码
这里我们先不考虑这种情况,StreamDemo
的 build
关于这部分的处理方法如下,这里对于吧 ChatMessageModel
也就不需要使用 watch
方法了,完全依赖于 StreamProvider
的流推送来更新组件。每次发送消息或接收消息后,构建时在返回组件树前就更新了消息列表数据了,因此也能保证数据是最新的。其实,相当于我们投机取巧实现了两个 Provider
之间的数据交互。
@override
Widget build(BuildContext context) {
Map<String, dynamic>? messageJson = context.watch<Map<String, dynamic>?>();
if (messageJson != null) {
context.read<ChatMessageModel>().addMessage(messageJson);
}
List<MessageEntity> messages = context.read<ChatMessageModel>().messages;
// ListView 部分
}
复制代码
运行效果
来看一下运行效果,模拟器的好处就是可以开多个调试。效果是实现了,不过实际即时聊天比这个复杂很多,而且一般也不会用 Socket
,但是如果 App 内部要实现应用打开后的即时消息推送,WebSocket
是一个不错的选择。源码已经提交,后端和 Flutter 代码分布如下:
评论