Flutter 入門與實戰(五十五):和 Provider 一起玩 WebSocket

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

這是我參與8月更文挑戰的第18天,活動詳情檢視:8月更文挑戰

前言

在實際的應用中,我們經常會遇到資料流的處理,比如監聽實時位置時會產生實時的位置資訊流,比如 Socket 通訊時需要監聽Socket 資料流。對於資料流,在 Flutter 中稱之為 Stream,而 ProviderStream專門設計了一個 StreamProvider 來監聽資料流的變化,當資料流產生新的資料時就會通知監聽元件重新整理。本篇我們就來介紹如何利用 StreamProvider 監聽 WebSocket 資料。

本篇設計的知識點如下:

  • WebSokcet 客戶端封裝外掛 socket_io_client的使用。
  • StreamProvider 監聽 Socket資料。

為了能夠讓程式正常執行,已經在後端程式碼增加了簡單的 socket,使用的是和 socket_io_client 配套的服務端socket.ionpm 地址為socket.io,版本為4.1.3。注意 Flutter 的外掛存在版本相容性,配套該版本的當前處於開發中的版本:^2.0.0-beta.4-nullsafety.0(穩定版的1.0.1外掛不支援服務端的4.1.3版本)。後端原始碼地址為:後端配套原始碼

socket_io_client 介紹

socket_io_client 是廣受歡迎的 Socket 客戶端外掛,javascript 也有對應的版本。socket_io_client 對 Dart 的 Socket 進行了封裝,從而簡化了 socket 的使用。

建立連線的方式如下,其中$host$port對應主機名和埠,後面的是配置引數。使用websocket 時,需要指定 transports(陣列)的元素包括 websocketautoConnect 為建立後是否自動連線,預設會自動連線。forceNew 為是否每次建立新的 Socket 物件,如果為 false(預設) 則會使用舊的連線(沒有關閉的情況下)。這裡我們使用的是每次使用新的 Socket 物件 這是因為退出頁面後我們要關閉連線。

_socket = SocketIO.io('ws://$host:$port', <String, dynamic>{
  'transports': ['websocket'],
  'autoConnect': true,
  'forceNew': true
});
複製程式碼

建立好之後就可以設定 Socket的事件響應回撥方法,常用的由如下方法:

  • onConnect(EventHandler handler):連線建立成功回撥;
  • onConnectTimeout(EventHandler handler):連線超時回撥;
  • onConnectError(EventHandler handler):連線錯誤回撥;
  • onError(EventHandler handler):發生錯誤時回撥;
  • on(String event, (EventHandler handler):訂閱指定事件的訊息,服務端傳送該事件的訊息時可以在該函式接收。
  • onDisconnect(EventHandler handler):斷開連線時回撥;
  • connect/disconnect:主動連線/斷開連線方法;
  • open/close:開啟和關閉方法。

重點是建立連線後,當接收到的資料加入到一個流處理物件中。Flutter 提供了StreamController 類處理 Stream 物件。StreamController 只允許有一個訂閱者,可以使用 sink 屬性的 add 方法新增新的流資料,完成新增後會通知其訂閱者有新的資料產生。因此我們可以使用 StreamProvider 來訂閱這個流資料。

我們構建一個 Socket 管理類 StreamSocket,程式碼如下:

class StreamSocket {
  final _socketResponse = StreamController<String>();

  Stream<String> get getResponse => _socketResponse.stream;

  final String host;
  final int port;
  late final Socket _socket;

  StreamSocket({required this.host, required this.port}) {
    _socket = SocketIO.io('ws://$host:$port', <String, dynamic>{
      'transports': ['websocket'],
      'autoConnect': true,
      'forceNew': true
    });
  }

  void connectAndListen() {
    _socket.onConnect((_) {
      debugPrint('connected');
    });

    _socket.onConnectTimeout((data) => debugPrint('timeout'));
    _socket.onConnectError((error) => debugPrint(error.toString()));
    _socket.onError((error) => debugPrint(error.toString()));
    _socket.on('msg', (data) {
      _socketResponse.sink.add(data);
    });
    _socket.onDisconnect((_) => debugPrint('disconnect'));
  }

  void sendTextMessage(String message) {
    _socket.emit('msg', message);
  }

  void close() {
    _socketResponse.close();
    _socket.disconnect().close();
  }
}
複製程式碼

StreamProvider 應用

StreamProvider 的應用和 FutureProvider,ChangeNotifierProvider 類似,也分為create 和 value兩種形式:

// create 形式
StreamProvider({
  Key? key,
  required Create<Stream<T>?> create,
  required T initialData,
  ErrorBuilder<T>? catchError,
  UpdateShouldNotify<T>? updateShouldNotify,
  bool? lazy,
  TransitionBuilder? builder,
  Widget? child,
}) 
  
// value 形式
StreamProvider.value({
  Key? key,
  required Stream<T>? value,
  required T initialData,
  ErrorBuilder<T>? catchError,
  UpdateShouldNotify<T>? updateShouldNotify,
  bool? lazy,
  TransitionBuilder? builder,
  Widget? child,
}) 
複製程式碼

這裡我們首先使用一個 StatefulWidget 來管理 StreamSocket 的初始化以及 Socket 的關閉,並將實際業務元件用兩個 Provider 包裹,一個是 StreamProvider,一個是 MessageModelStreamProvider用於自動監聽 Socket 的資料流,並顯示在介面上;MessageModel 目前僅用於傳送訊息。

class _SocketClientWrapperState extends State<SocketClientWrapper> {
  final StreamSocket streamSocket = StreamSocket(host: '127.0.0.1', port: 3001);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stream Provicer'),
      ),
      body: Stack(
        alignment: Alignment.bottomCenter,
        children: [
          StreamProvider<String>(
            create: (context) => streamSocket.getResponse,
            initialData: '',
            child: StreamDemo(),
          ),
          ChangeNotifierProvider<MessageModel>(
            child: MessageReplyBar(messageSendHandler: (message) {
              streamSocket.sendTextMessage(message);
            }),
            create: (context) => MessageModel(),
          ),
        ],
      ),
    );
  }

  @override
  void initState() {
    streamSocket.connectAndListen();
    super.initState();
  }

  @override
  void dispose() {
    streamSocket.close();
    super.dispose();
  }
}
複製程式碼

這裡我們使用了 Stack將訊息的輸入條MessageReplyBar固定在底部,需要注意的是,如果想要輸入條自動隨著鍵盤浮動,則需要將其放入到 Scaffoldbody 裡,否則會一直固定在底部而被遮擋住。

StreamDemoMessageReplyBar兩個元件都很簡單,業務邏輯為 MessageReplyBar點選傳送按鈕將訊息通過Socket 發出到服務端。然後 StreamDemo 回顯 StreamProviderSocket 接收到的服務端訊息。

執行效果

我們配合服務端的列印日誌來看執行效果:可以看到連線的建立、傳送訊息、接收訊息和斷開連線的過程都是正常的。

螢幕錄製2021-08-17 下午10.18.16.gif

原始碼已上傳至:狀態管理相關程式碼 - null safety 版本

總結

本篇介紹了ProviderStreamProvider流狀態管理,同時引入了 socket_io_client外掛實現了與服務端的 WebSocket 通訊,通過StreamProvider訂閱流資料,並通知介面重新整理,實現了介面層的無侵入Socket資料展示。這一篇比較簡單,更多地是介紹兩個工具的使用,下一篇我們用這兩個工具來實現一個即時聊天應用。


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

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

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

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

相關文章