Flutter 入門與實戰(五十七):兩個模擬器來聊天 — Provider 綜合應用

島上碼農發表於2021-08-20

這是我參與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}');
  },
);
複製程式碼

聊天介面的實現

我們將傳送的訊息放在右邊,將接收到的訊息放在左邊,距做還是居右通過 Containermargin 來實現。至於區分,通過訊息物件的fromUserId 來區分,如果 fromUserId 和當前使用者id 一致,則是傳送出去的訊息,否則就是接收到的訊息。在這裡我們因為還沒有使用者體系,先將當前的使用者 id 寫死。為了實現模擬器之間的聊天,我們一個模擬器設定為 user1,一個設定為 user2。介面也是使用ListView.builder(萬能不?)構建。

image.png

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 方法新增訊息。但是這樣會增加兩個類的耦合。還有一種方式是取巧的方式了,那就是 StreamdDemobuild 方法能夠獲取到 StreamSocket 推送的最新訊息,在這裡讀取到最新的訊息後就可以新增到訊息列表了。由於前面我們傳送訊息和接收訊息都將訊息加入到了訊息流中,這樣處理方式就統一了。

這種方式需要注意,Provider 不允許在元件的build 方法中再次呼叫類似 notifyListeners 的方法通知該元件重新整理,因此在 ChatMessageModeladdMessage 方法裡不可以使用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));
  }
}
複製程式碼

這裡我們先不考慮這種情況,StreamDemobuild關於這部分的處理方法如下,這裡對於吧 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 相關篇章,具體如下:

內容基本覆蓋了 Provider 的使用,如果想深入研究的也可以自行去看官方示例和原始碼。


我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章,對應原始碼請看這裡:Flutter 入門與實戰專欄原始碼

??:覺得有收穫請點個贊鼓勵一下!

?:收藏文章,方便回看哦!

?:評論交流,互相進步!

相關文章