← 使用情境

iOS、Android 與網頁上的 Flutter

一個 Dart 用戶端:私有頻道、自動重連,App 關閉時改用推播。

Flutter 用一份程式碼做出 iOS、Android、網頁與桌面應用程式,即時功能的程式碼也該是一份。官方的 Pusher Flutter 外掛一律連到 pusher.com 的主機,所以請改用 dart_pusher_channels:一個純 Dart 的 Pusher 協定用戶端,可以設定 host,而且 Flutter 能跑的地方它都能跑。

使用者得到什麼

  • 訂單、聊天與比分畫面就地更新,不必下拉重新整理。
  • 經過隧道、搭電梯,或從 Wi-Fi 切到行動網路後,連線會自己回來。
  • 手機上和瀏覽器裡的行為一樣。

用到的功能

  • dart_pusher_channels,整個 App 共用一個用戶端。
  • 私有頻道(private-),由你的後端用已登入使用者的 token 授權。
  • 私有與 presence 頻道上的用戶端事件,用於輸入中提示與其他小訊號。
  • App 在背景或已關閉時,改用 Hibira Push。

步驟 1:安裝

flutter pub add dart_pusher_channels

步驟 2:整個 App 共用一個用戶端

// lib/realtime.dart
import 'package:dart_pusher_channels/dart_pusher_channels.dart';

final realtime = PusherChannelsClient.websocket(
  options: PusherChannelsOptions.fromHost(
    scheme: 'wss',
    host: 'ws.hibira.io',
    port: 443,
    key: const String.fromEnvironment('HIBIRA_KEY'),
  ),
  connectionErrorHandler: (error, trace, refresh) => refresh(),
);

建置時傳入 key:flutter run --dart-define=HIBIRA_KEY=YOUR_APP_KEY。App secret 絕不放進 App;私有頻道由你的後端簽署。

步驟 3:在畫面上追蹤一筆訂單

// lib/order_screen.dart
import 'dart:async';
import 'package:dart_pusher_channels/dart_pusher_channels.dart';
import 'package:flutter/material.dart';
import 'realtime.dart';

class OrderScreen extends StatefulWidget {
  const OrderScreen({super.key, required this.orderId, required this.token});
  final String orderId;
  final String token;

  @override
  State<OrderScreen> createState() => _OrderScreenState();
}

class _OrderScreenState extends State<OrderScreen> {
  late final PrivateChannel channel;
  final subs = <StreamSubscription>[];
  String status = '…';

  @override
  void initState() {
    super.initState();
    channel = realtime.privateChannel(
      'private-order-${widget.orderId}',
      authorizationDelegate: EndpointAuthorizableChannelTokenAuthorizationDelegate.forPrivateChannel(
        authorizationEndpoint: Uri.parse('https://api.example.com/pusher/auth'),
        headers: {'Authorization': 'Bearer ${widget.token}'},
      ),
    );
    subs.add(channel.bind('status').listen((event) {
      setState(() => status = event.tryGetDataAsMap()?['status'] ?? status);
    }));
    subs.add(realtime.onConnectionEstablished.listen((_) => channel.subscribeIfNotUnsubscribed()));
    channel.subscribeIfNotUnsubscribed();
  }

  @override
  void dispose() {
    for (final s in subs) {
      s.cancel();
    }
    channel.unsubscribe();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Text(status);
}

在 onConnectionEstablished 時重新訂閱,每次重新連線後頻道都會回來。你的後端授權端點會收到 socket_id 與 channel_name,檢查 token 後,用任何相容 Pusher 的伺服器函式庫回傳簽章。

步驟 4:App 開著時才連線

class _AppState extends State<App> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    realtime.connect();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) realtime.disconnect();
    if (state == AppLifecycleState.resumed) realtime.connect();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => const MaterialApp(home: HomeScreen());
}

iOS 與 Android 本來就會暫停背景 App 的網路,所以在 paused 時斷線,resumed 時再連上。App 關閉時使用者一定要知道的事,請用推播通知送出:請看把使用者帶回來的推播通知與推播指南,其中介紹了你的 Flutter 宿主 App 可以呼叫的 Android 與 iOS SDK。

小提醒

  • 先從你的 API 載入畫面需要的資料,再讓事件更新它;打開時就必須存在的狀態,請發布到 cache- 頻道。
  • 在應用程式設定中開啟 Allow client events 後,用戶端事件(channel.trigger(eventName: 'client-typing', data: {...}))就能在私有與 presence 頻道上使用。
  • 登入後,Flutter 指南會用你的應用程式數值填好用戶端設定。