← ユースケース

iOS・Android・Web の Flutter

Dart クライアント 1 つで、プライベートチャンネル、再接続、閉じているときはプッシュ。

Flutter は 1 つのコードから iOS、Android、Web、デスクトップのアプリを作ります。リアルタイムのコードも 1 つであるべきです。公式の Pusher Flutter プラグインは常に pusher.com のホストに接続するので、代わりに dart_pusher_channels を使います。ホストを設定できる純 Dart の Pusher プロトコルクライアントで、Flutter が動く場所ならどこでも動きます。

ユーザーにとっての価値

  • 注文、チャット、スコアの画面がその場で更新され、引っ張って更新する必要はありません。
  • トンネルやエレベーター、Wi-Fi からモバイル回線への切り替えのあとも、接続は自動で戻ります。
  • スマートフォンでもブラウザでも同じ動き。

使う機能

  • アプリ全体で 1 つのクライアントにする dart_pusher_channels。
  • サインイン中のユーザーのトークンでバックエンドが認可するプライベートチャンネル(private-)。
  • 入力中表示などの小さな合図に使う、プライベート/プレゼンスチャンネルのクライアントイベント。
  • アプリがバックグラウンドや終了しているときの Hibira Push。

ステップ 1:インストール

flutter pub add dart_pusher_channels

ステップ 2:アプリ全体で 1 つのクライアント

// 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(),
);

キーはビルド時に渡します:flutter run --dart-define=HIBIRA_KEY=YOUR_APP_KEY。アプリのシークレットは決してアプリに入れません。プライベートチャンネルはバックエンドが署名します。

ステップ 3:画面で 1 件の注文を追う

// 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 を受け取り、トークンを確認して、Pusher 互換のサーバーライブラリで作った署名を返します。

ステップ 4:アプリを開いている間だけ接続する

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 はバックグラウンドのアプリの通信をどのみち止めるので、paused で切断し、resumed で接続し直します。アプリを閉じている間に伝えるべきことは、プッシュ通知で送りましょう。ユーザーを呼び戻すプッシュ通知と、Flutter のホストアプリから呼べる Android/iOS SDK を説明したプッシュのガイドを参照してください。

ヒント

  • 画面に必要なものはまず API から読み込み、あとはイベントで更新します。開いた瞬間に必要な状態は cache- チャンネルに配信しましょう。
  • アプリ設定で Allow client events をオンにすると、クライアントイベント(channel.trigger(eventName: 'client-typing', data: {...}))をプライベート/プレゼンスチャンネルで使えます。
  • サインインすると、Flutter ガイドにアプリの値を埋めたクライアント設定が表示されます。