(当頁はdocs.flutter.dev(https://docs.flutter.dev/cookbook/networking/web-sockets)のリファレンスブログです。)
(通常のHTTPリクエストに加えて) WebSocketを使用してサーバと接続することができます。
WebSocket は、ポーリングを必要とせずにサーバとの双方向通信を可能にします。
ここでは、Lob.com が提供するテスト用のWebSocketサーバに接続します。
(このサーバは、送信したメッセージをそのまま返す動作をします。)
1.WebSocket サーバに接続する
WebSocket通信を行うために、web_socket_channel パッケージを使用します。
このパッケージは、WebSocketサーバへの接続を簡単に行うためのツールを提供します。
WebSocketChannel を使うことで、サーバからのメッセージを受信したり、サーバにメッセージを送信したりすることができます。
//WebSocketChannel.connect:
//( 指定した WebSocket サーバへの接続を確立するためのメソッドです。
// 引数には接続先のサーバの URL を指定します。)
final channel = WebSocketChannel.connect(
//Uri.parse():
//( 文字列として指定された URL を Uri オブジェクトに変換します。
// これにより、WebSocketChannel.connect() メソッドで
// 適切に URL を渡せるようになります。)
Uri.parse(
'wss://echo.websocket.events'
//↑ これは、テスト用の WebSocket サーバで、
// 送信したメッセージをそのまま返してくれる「エコーサーバ」です。
// wss は、WebSocket Secure(暗号化された通信)の略で、
// ws(非暗号化)より安全に通信できます。
),
);
2.サーバからのメッセージをリッスンする
WebSocket サーバとの接続が確立されたら、サーバからのメッセージをリッスンして処理します。
(テスト用サーバの場合、送信したメッセージがそのまま返されます(エコーサーバの動作)。
この例では、StreamBuilder ウィジェットを使用して新しいメッセージをリッスンし、受信したデータを Text ウィジェットで画面に表示します。
//StreamBuilder:
//( Stream と連携するための Flutter ウィジェット。
// ストリーム内の新しいイベントを検知するたびに、UI を再構築します。)
StreamBuilder(
//stream:
//( WebSocketChannel のストリームを指定 )
stream: channel.stream, //←上記設定サーバからのメッセージストリーム。
//( ↑サーバーからの新しいメッセージが channel.stream に届くたびに、
// StreamBuilder が反応します。)
//builder:
//( ストリームで新イベント(メッセージ)発生時に呼出されるコールバック関数。)
builder: (context, snapshot) {
// スナップショットにデータがある場合は表示、なければ空文字列
return Text(snapshot.hasData ? '${snapshot.data}' : '');
},
)
3.サーバにデータを送信する
サーバにデータを送信するには、WebSocketChannel が提供する sink を使います。
この sink を通じて、サーバにメッセージを送ることができます。
//サーバーとの接続
final channel = WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
);
//channel.sink:
//( WebSocketChannel が提供する sink は、サーバへのデータ送信をサポートします。
// add() メソッド を使用して、サーバにデータ(メッセージ)を送信します。
// メッセージを sink に追加することで、サーバーにデータを送信します。)
channel.sink.add('Hello!');
//サーバからのレスポンスをリッスン
channel.stream.listen((message) {
print('Received: $message'); //サーバーからのメッセージを表示
});
4.WebSocket 接続を閉じる
WebSocket を使用し終わったら、接続を閉じる必要があります。
これにより、サーバとの通信を終了し、リソースが解放されます。
import 'package:web_socket_channel/web_socket_channel.dart';
void main() {
//WebSocket サーバーに接続
final channel = WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
);
//サーバーにデータを送信
channel.sink.add('Hello, server!');
//サーバーからのメッセージをリッスン
channel.stream.listen((message) {
print('Received: $message');
//メッセージを受信後に接続を閉じる
channel.sink.close();
});
}
5.サンプルコード
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
const title = 'WebSocket Demo';
return const MaterialApp(
title: title,
home: MyHomePage(
title: title,
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
super.key,
required this.title, //表示するタイトルを受け取る
});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
//ユーザが入力したメッセージを管理するテキストコントローラ
final TextEditingController _controller = TextEditingController();
//WebSocket サーバに接続
final _channel = WebSocketChannel.connect(
//Uri.parse('wss://echo.websocket.events'), //エコーサーバの URL
Uri.parse('wss://ws.postman-echo.com/raw'), //エコーサーバの URL
);
//追加1
late final Stream _broadcastStream;
@override
void initState() {
super.initState();
// WebSocketからのメッセージをリッスンしてログに出力
//_channel.stream.listen((message) {
_broadcastStream = _channel.stream.asBroadcastStream();
if (kDebugMode) {
//print('Received message: $message');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title), //アプリバーにタイトルを表示
),
body: Padding(
padding: const EdgeInsets.all(20), //画面全体のパディング
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, //左揃え
children: [
//ユーザがメッセージを入力するフォーム
Form(
child: TextFormField(
controller: _controller, //入力されたテキストを管理
decoration: const InputDecoration(labelText: 'Send a message'), //入力フォームのラベル
),
),
const SizedBox(height: 24), //フォームとメッセージ表示の間にスペースを追加
//サーバからのメッセージを表示する StreamBuilder
StreamBuilder(
//stream: _channel.stream, //WebSocket サーバからのストリームを設定
stream: _broadcastStream, //WebSocket サーバからのストリームを設定
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
//スナップショットにデータがあれば表示、なければ'No data received yet'表示
return Text(snapshot.hasData
? '${snapshot.data}'
: 'No data received yet');
} else if (snapshot.connectionState == ConnectionState.waiting) {
return const Text('Waiting for data...');
} else {
return const Text('Connection closed');
}
},
)
],
),
),
//送信ボタン
floatingActionButton: FloatingActionButton(
onPressed: _sendMessage, //メッセージを送信するメソッドを呼び出す
tooltip: 'Send message', //ボタンのツールチップ
child: const Icon(Icons.send), //ボタンのアイコン
),
);
}
//メッセージをサーバに送信するメソッド
void _sendMessage() {
if (_controller.text.isNotEmpty) {
_channel.sink.add(_controller.text); //ユーザが入力したメッセージをサーバに送信
}
}
//ウィジェットが破棄される際にリソースを解放
@override
void dispose() {
_channel.sink.close(); //WebSocket の接続を閉じる
_controller.dispose(); //テキストコントローラのリソースを解放
super.dispose();
}
}
docs.flutter.dev(https://docs.flutter.dev/cookbook/networking/web-sockets)中のサンプルだと、以下の様なエラーが発生しましたので、2か所ほど大きく変更してあります。
(変更点)
・WebSocket応答テスト用サーバ:
’wss://ws.postman-echo.com/raw’ に変更
・Stream:
Stream(シングルリスナー用)を、asBroadcastStream() (複数リスナーが可能な Broadcast Stream に変換可能)に変更
(エラー)
════════ Exception caught by widgets library ═══════════════════════════════════
The following StateError was thrown building KeyedSubtree-[GlobalKey#80518]:
Bad state: Stream has already been listened to.
The relevant error-causing widget was:
Scaffold Scaffold:file:///C:/app/flutter/test02/websockets01/lib/main.dart:58:12
When the exception was thrown, this was the stack:
#3 _StreamBuilderBaseState._subscribe (package:flutter/src/widgets/async.dart:130:38)
async.dart:130
#4 _StreamBuilderBaseState.initState (package:flutter/src/widgets/async.dart:104:5)
async.dart:104
#5 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5748:55)
framework.dart:5748
#6 ComponentElement.mount (package:flutter/src/widgets/framework.dart:5593:5)
framework.dart:5593
#7 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4468:16)
framework.dart:4468
#8 MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:7035:36)
framework.dart:7035
#9 MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:7047:32)
framework.dart:7047
... Normal element mounting (28 frames)
#37 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4468:16)
framework.dart:4468
#38 MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:7035:36)
framework.dart:7035
#39 MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:7047:32)
framework.dart:7047
... Normal element mounting (340 frames)
#379 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4468:16)
framework.dart:4468
#380 MultiChildRenderObjectElement.inflateWidget (package:flutter/src/widgets/framework.dart:7035:36)
framework.dart:7035
#381 MultiChildRenderObjectElement.mount (package:flutter/src/widgets/framework.dart:7047:32)
framework.dart:7047
... Normal element mounting (525 frames)
#906 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4468:16)
framework.dart:4468
#907 Element.updateChild (package:flutter/src/widgets/framework.dart:3963:18)
framework.dart:3963
#908 _RawViewElement._updateChild (package:flutter/src/widgets/view.dart:441:16)
view.dart:441
#909 _RawViewElement.mount (package:flutter/src/widgets/view.dart:464:5)
view.dart:464
... Normal element mounting (15 frames)
#924 Element.inflateWidget (package:flutter/src/widgets/framework.dart:4468:16)
framework.dart:4468
#925 Element.updateChild (package:flutter/src/widgets/framework.dart:3963:18)
framework.dart:3963
#926 RootElement._rebuild (package:flutter/src/widgets/binding.dart:1605:16)
binding.dart:1605
#927 RootElement.mount (package:flutter/src/widgets/binding.dart:1574:5)
binding.dart:1574
#928 RootWidget.attach.<anonymous closure> (package:flutter/src/widgets/binding.dart:1527:18)
binding.dart:1527
#929 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:3038:19)
framework.dart:3038
#930 RootWidget.attach (package:flutter/src/widgets/binding.dart:1526:13)
binding.dart:1526
#931 WidgetsBinding.attachToBuildOwner (package:flutter/src/widgets/binding.dart:1265:27)
binding.dart:1265
#932 WidgetsBinding.attachRootWidget (package:flutter/src/widgets/binding.dart:1247:5)
binding.dart:1247
#933 WidgetsBinding.scheduleAttachRootWidget.<anonymous closure> (package:flutter/src/widgets/binding.dart:1233:7)
binding.dart:1233
#937 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
isolate_patch.dart:184
(elided 6 frames from class _Timer, dart:async, and dart:async-patch)