iT邦幫忙

2024 iThome 鐵人賽

DAY 14
1
Mobile Development

從零開始以Flutter打造跨平台聊天APP系列 第 14

Day-14 在 Flutter 中使用 Websocket 及 StreamBuilder

  • 分享至 

  • xImage
  •  

Generated from Stable Diffusion 3 Medium

WebSocket 是一種在單個 TCP 連接上進行全雙工通信的協議。與傳統的 HTTP 通信不同,WebSocket 允許伺服器和客戶端在建立連接後,能夠雙向傳遞數據,且不必每次都發起新的請求。這使得 WebSocket 成為實時應用比如:聊天應用、遊戲、即時通知...等,一個理想的解決方案。

本次教學會使用 Go 語言建立 Websocket 伺服器及使用第三方套件在 Flutter 中使用 websocket。

範例程式

使用 Go 語言建立一個 Websocket 伺服器

先使用 go mod 初始化 go 語言專案

go mod init my-websocket

我們的教學習慣盡可能以官方的套件來實作,因此我們選用 golang 補充套件 golang.org/x/net/websocket

package main

import (
	"fmt"
	"net/http"

	"golang.org/x/net/websocket"
)

const port = 8080

func main() {
	// 設定 WebSocket 路由
	http.Handle("/ws", websocket.Handler(handleWebSocket))

	// 啟動伺服器
	fmt.Printf("WebSocket 伺服器運行於: %d\n", port)
	err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
	if err != nil {
		fmt.Println("伺服器啟動失敗:", err)
	}
}

func handleWebSocket(ws *websocket.Conn) {
	var message string

	for {
		// 讀取客戶端發送的消息
		err := websocket.Message.Receive(ws, &message)
		if err != nil {
			fmt.Println("讀取消息錯誤:", err)
			break
		}

		fmt.Println("收到消息:", message)

		m := []rune(message)
		for i := 0; i < len(m)/2; i++ {
			m[i], m[len(m)-1-i] = m[len(m)-1-i], m[i]
		}

		// 回傳消息給客戶端
		err = websocket.Message.Send(ws, string(m))
		if err != nil {
			fmt.Println("發送消息錯誤:", err)
			break
		}
	}
}

當程式執行時,我們可以透過 ws://localhost:8080/ws 做互動。我們將收到訊息前後反轉並回傳給 client!值得一提的是,當連線建立後,即使 client 端不發送封包,server 端仍然可以主動傳訊息!

# 執行 go 語言程式
go run main.go

# 或者編譯 go 語言程式
go build main.go
./main

如何使用 StreamBuilder

前幾天我們有介紹了 FutureBuilder Day-11 在 Flutter 中使用 FutureBuilder 進行狀態管理,FutureBuilder 可以處理異步函式,使其在未取得值時顯示一個畫面,取得值時顯示另一個畫面,而當錯誤發生時,又可以顯示另一個畫面。

在 Flutter 中,除了使用 FutureBuilder 來處理一次性的異步操作,還有另一個更靈活的工具,適合處理連續異步資料流,那就是 StreamBuilderStreamBuilder 專門用來監聽 Stream 的變化,並根據不同的狀態來更新 UI。這對於需要持續接收數據的應用場景特別實用,比如 Websocket 連接、實時數據更新、傳感器數據等。

Stream 可以理解為一個持續傳送資料的通道,不斷地傳遞資料給訂閱者,而這些資料可能是同步或異步的。和 Future 不同,Stream 不只會返回一個結果,而是不斷發送多個事件。

StreamBuilder 的使用方法與 FutureBuilder 相似,StreamBuilder 通常會處理五個狀態:

  • 發生錯誤時
  • 尚未連線
  • 已連線,尚未收到數據
  • 收到數據時
  • 數據全數接收完異
StreamBuilder<int>(
  stream: counterStream(),
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return Text('錯誤 ${snapshot.error}');
    } else {
      switch (snapshot.connectionState) {
        case ConnectionState.none:
          return const Text('目前尚未連線到任何異步計算');
        case ConnectionState.waiting:
          return const Text('已連線,等待互動');
        case ConnectionState.active:
          return Text('資料傳輸中:${snapshot.data}');
        case ConnectionState.done:
          return const Text('資料傳輸完成');
      }
    }
  },
)

那麼,該如何製作 Stream() 這個物件呢?我們可以使用 asynchronous generator (異步生成器)。該生成器允許使用者使用 yield 發送值到 Stream 中。

Stream<int> getStream() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield i; // 每秒發出一個整數
  }
}

demo of stream builder

使用 Flutter 建立一個 Websocket 客戶端

其實原本是想要直接從頭來的,但礙於功力不足,因此,這裡會直接介紹如何使用第三方套件 web_socket_channel 來實作。官方文件:web_socket_channel | Dart package

首先,一如往常,我們先安裝該套件

flutter pub add web_socket_channel

接著我們看一下文檔,有一個 dart 中的範例程式:

import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;

main() async {
  final wsUrl = Uri.parse('ws://example.com');
  final channel = WebSocketChannel.connect(wsUrl);

  await channel.ready;

  channel.stream.listen((message) {
    channel.sink.add('received!');
    channel.sink.close(status.goingAway);
  });
}

我們可以稍微改良後,在 flutter 中做使用

class _WebSocketExampleState extends State<WebSocketExample> {
  // 使用 WebSocketChannel 建立連接
  // 網址的部分根據我們的伺服器做調整
  final WebSocketChannel channel = WebSocketChannel.connect(
    Uri.parse('ws://localhost:8080/ws'),
  );

  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    channel.sink.close();
    _controller.dispose();
    super.dispose();
  }

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      channel.sink.add(_controller.text);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('WebSocket Example'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          children: <Widget>[
            TextField(
              controller: _controller,
              decoration: const InputDecoration(labelText: 'Send a message'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _sendMessage,
              child: const Text('Send'),
            ),
            const SizedBox(height: 20),
            StreamBuilder(
              stream: channel.stream, // 監聽 WebSocket 的數據流
              builder: (context, snapshot) {
                if (snapshot.hasError) {
                  return Text('錯誤 ${snapshot.error}');
                } else {
                  switch (snapshot.connectionState) {
                    case ConnectionState.none:
                      return const Text('目前尚未連線到任何異步計算');
                    case ConnectionState.waiting:
                      return const Text('已連線,等待互動');
                    case ConnectionState.active:
                      return Text('資料傳輸中:${snapshot.data}');
                    case ConnectionState.done:
                      return const Text('資料傳輸完成');
                  }
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

demo of websocket


後記:有點想試試從 dart:iodart:html 分別實作 websocket,可能後續再補充吧!


上一篇
Day-13 在 Flutter 中以 JS 及 Kotlin 實作 Shared preferences 保存資料
下一篇
Day-15 在 Flutter 中自動生成 JSON 序列化程式碼並撰寫單元測試
系列文
從零開始以Flutter打造跨平台聊天APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言