
本文同步發佈於毛毛的踩坑人生
回到台灣了
終於不用思思念念怕鐵人賽斷更🤣
昨天在 Golang 那邊建立了一個 WebSocket 通道
雖然還沒把資料放進來
但可以先小跳一點到 Flutter 這邊
先把 WebSocket 打通
最後再把即時價格透過這個通道送下來
這樣就可以第一手顯示最新的價格~
這邊我們使用的 dart 套件是 web_socket_channel
沒有非用它不可的原因
僅是我用起來比較順手
硬要比較 Like 數量的話,還有替代方案是 socket_io_client
這篇文章就先使用 web_socket_channel: ^2.4.0
首先在 pubspec.yaml 的 dependencies 加入以下
dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  web_socket_channel: ^2.4.0
# 記得要安裝
flutter pub get --no-example
作者提供的範例實在是太少(下圖)
按照他這樣寫不是不行
但絕對不會是一個合格的實作
幫各位踩了一些坑🤣

這邊把連線包成一個方法
可以看得出來,比範例還要多一些東西
後面會解釋為什麼需要
但是裡面還沒有實作資料進來的部分哦
import 'package:web_socket_channel/io.dart';
void initialWS() {
  _channel = IOWebSocketChannel.connect(Uri.parse('ws://127.0.0.1:8080/ws/time'), pingInterval: const Duration(seconds: 1));
  _channel!.stream.listen(
    (message) {
      if (!mounted) {
        return;
      }
    },
    onDone: () {
      if (mounted) {
        Future.delayed(const Duration(milliseconds: 1000)).then((value) {
          _channel!.sink.close();
          initialWS();
        });
      }
    },
    onError: (error) {},
  );
}
還記得我們在 DAY-11 有提到 Flutter 是有兩種 Widget 的
分別是 StatelessWidget、StatefulWidget
很明顯這個串流即時價格的功能會是有狀態的
生命週期應該會是進入這個頁面,然後建立一個通道
在 StatefulWidget 中,初始化的方法就是 initState()
@override
void initState() {
  initialWS();
  super.initState();
}
跟大部分 OOP 語言一樣
透過 override,我們可以在超類的 initState 裡面去做一些變更
這邊我們所做的就是把連線這件事加入
dispose() 也是 StatefulWidget 獨有的方法
他的作用是當這個畫面不在被渲染時就會執行
直白的說就是使用者切換到其他畫面時
同樣我們要來 override
@override
void dispose() {
  _channel!.sink.close();
  super.dispose();
}
很容易看得出來,這邊就是很簡單做了主動關閉
不關也行,但後端是我們自己寫的
能關就關嘛🤓
我們在 DAY-18 有在 Golang 這邊開了一個暫時性的 WebSocket
下面這個 URL
/ws/time
# 給他 time 就會回覆 Current time
我們先暫時把 APP
改成以下這樣
Future<String> currentTime = Future<String>.value('');
@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Try WebSocket'),
      ),
      body: FutureBuilder<String>(
        future: currentTime,
        builder: (context, snapshot) {
          if (snapshot.hasData && snapshot.data!.isNotEmpty) {
            return Center(
              child: Text(snapshot.data!),
            );
          }
          return const Center(
            child: CircularProgressIndicator(
              color: Colors.black,
            ),
          );
        },
      ),
    ),
  );
}
畫面上應該如下

然後我們再加一個方法
可以送出 time 給 Golang 那邊
void sendReq() {
  _channel!.sink.add('time');
}
好像漏了什麼
需要交互,所以還是把按鈕加回來
appBar: AppBar(
  title: const Text('Try WebSocket'),
  actions: [
    IconButton(
      icon: const Icon(Icons.timer),
      onPressed: sendReq,
    ),
  ],
),

合起來看看
順便砍掉一些多餘字元
@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
      appBar: AppBar(
        title: const Text('Try WebSocket'),
        actions: [
          IconButton(
            icon: const Icon(Icons.timer),
            onPressed: sendReq,
          ),
        ],
      ),
      body: FutureBuilder<String>(
        future: currentTime,
        builder: (context, snapshot) {
          if (snapshot.hasData && snapshot.data!.isNotEmpty) {
            return Center(
              child: Text(
                snapshot.data!.toString().substring(0, 33),
                style: const TextStyle(fontSize: 20),
              ),
            );
          }
          return const Center(
            child: CircularProgressIndicator(
              color: Colors.black,
            ),
          );
        },
      ),
    ),
  );
}
試著按按看右上角的碼表
畫面中央應該出現 Golang 透過 WebSocket 送過來的時間

看看 terminal
這不是呼叫 API
而是實打實的 WebSocket 的訊息交換
flutter: send time req
flutter: receive time: Current time: 2023-10-04 21:22:48.887227 +0800 CST m=+1444.888261751
flutter: send time req
flutter: receive time: Current time: 2023-10-04 21:22:49.50685 +0800 CST m=+1445.507884210
flutter: send time req
flutter: receive time: Current time: 2023-10-04 21:22:50.115684 +0800 CST m=+1446.116718293
flutter: send time req
flutter: receive time: Current time: 2023-10-04 21:22:50.659663 +0800 CST m=+1446.660698085
雖然今天這樣的展示
有那麼一點不像是 WebSocket
但我們知道其實這不是呼叫 API
明天就可以把正式的資料丟進來讓他一直一直跳動了~