這是我參與8月更文挑戰的第20天,活動詳情檢視:8月更文挑戰
前言
我們在前面花了很大篇幅介紹 Provider
狀態管理,這是因為在 Flutter 中,Provider
是眾多狀態管理外掛的首選。本篇以即時聊天為例,來講述 Provider
的綜合應用,也算是 Provider
狀態管理系列的終結篇。本篇涉及的內容如下:
- 聯絡人介面的構建;
- 聊天介面的簡單實現;
- StreamProvider 接收 Socket流資料並自動通知介面重新整理;
- MultiProvider為聊天主介面提供多個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 程式碼分佈如下:
總結
Provider 部分的程式碼我們就先介紹到這裡,接下來我們將介紹其他的狀態管理外掛。回顧一下 Provider 相關篇章,具體如下:
- Flutter 入門與實戰(四十七):使用 Provider 改造?一樣的程式碼,程式碼量降低了2/3!
- Flutter 入門與實戰(四十八):使用MultiProvider實現多狀態同時管理
- Flutter 入門與實戰(四十九):Provider 可以做巢狀狀態管理嗎?
- Flutter 入門與實戰(五十): Provider實現不相關頁面狀態共享
- Flutter 入門與實戰(五十三):仿掘金個人主頁,學習 FutureProvider 狀態管理
- Flutter 入門與實戰(五十四):Provider 之監聽狀態的區域性變化
- Flutter 入門與實戰(五十五):和 Provider 一起玩 WebSocket
- Flutter 入門與實戰(五十六):讓模擬器和和郵遞員(Postman)聊聊天
內容基本覆蓋了 Provider 的使用,如果想深入研究的也可以自行去看官方示例和原始碼。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!