📳

FlutterでWebsocketを使ってみる

2024/11/02に公開

🤔やってみたいこと

web_socket_channelというパッケージを使用してFlutterでソケット通信をやってみました。

そもそもソケット通信とは何かと言いますとこちらの記事でも解説しておりますがざっくり解説いたしますね。
https://zenn.dev/joo_hashi/articles/90c35772e753f5

WebSocketとは:

基本的な特徴

双方向通信が可能なプロトコル

サーバーとクライアント間で常時接続を維持
HTTPと違い、一度接続すれば継続的に通信可能
リアルタイムでデータをやり取りできる

こんな感じでね。これはローカルですが😅
https://youtu.be/E5ngqlTEG8o

使用例

  • チャットアプリケーション
  • リアルタイムゲーム
  • 株価や為替のリアルタイム更新
  • ライブ通知システム
  • オンライン協同作業ツール
  • 従来のHTTPとの違い

🚀やってみたこと

解説を参考にしつつ作ってみる。Goで作ったWebsocketが使えるAPIと通信をしてみる。Goのソースコードはリンクの記事のものを使ってみてください。

ちなみに今回は内容って、Goの知識もある人が使うものなのでFlutterしか知らない人は、他ので試したいという人いたら、Railsで同じ機能を実装するか別の手段を考えてください。


Docs and Usage

It provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel, an implementation that wraps dart:io's WebSocket class, and a similar implementation that wraps dart:html's.
It also provides constants for the WebSocket protocol's pre-defined status codes in the status.dart library. It's strongly recommended that users import this library with the prefix status.

ドキュメントと使用法
dart:io のWebSocketクラスをラップする実装と、dart:html のWebSocketクラスをラップする同様の実装を提供します。
また、status.dartライブラリでは、WebSocket プロトコルの定義済みステータスコード用の定数も提供します。このライブラリは、status.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);
  });
}

こちらが今回使用したサンプルです。ご興味ある方は試してみてください。

main.dart
import 'dart:convert';

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});

  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: WebSocketDemo(),
    );
  }
}

class WebSocketDemo extends StatefulWidget {
  const WebSocketDemo({super.key});

  
  _WebSocketDemoState createState() => _WebSocketDemoState();
}

class _WebSocketDemoState extends State<WebSocketDemo> {
  final TextEditingController _controller = TextEditingController();
  late WebSocketChannel channel;
  List<String> messages = [];

  
  void initState() {
    super.initState();
    connectToWebSocket();
  }

  void connectToWebSocket() {
    channel = WebSocketChannel.connect(
      Uri.parse('ws://localhost:8080/ws'),
    );

    channel.stream.listen(
      (dynamic message) {
        setState(() {
          try {
            // サーバーからのメッセージをデコード
            final decodedMessage = jsonDecode(message.toString());
            messages.add(decodedMessage['message'] ?? 'Invalid message');
          } catch (e) {
            messages.add('Error decoding message: $message');
          }
        });
      },
      onError: (error) {
        debugPrint('WebSocket Error: $error');
        // 必要に応じて再接続ロジックを実装
      },
      onDone: () {
        debugPrint('WebSocket Connection Closed');
        // 必要に応じて再接続ロジックを実装
      },
    );
  }

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      try {
        // メッセージをJSONフォーマットで送信
        final messageJson = jsonEncode({
          'message': _controller.text,
        });
        channel.sink.add(messageJson);
        _controller.clear();
      } catch (e) {
        debugPrint('Error sending message: $e');
      }
    }
  }

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('WebSocket Demo'),
        actions: [
          // 接続状態を表示するアイコン
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: connectToWebSocket,
            tooltip: '再接続',
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Form(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  labelText: 'メッセージを入力',
                  border: OutlineInputBorder(),
                ),
                onFieldSubmitted: (_) => _sendMessage(),
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _sendMessage,
              style: ElevatedButton.styleFrom(
                padding:
                    const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
              ),
              child: const Text('送信'),
            ),
            const SizedBox(height: 16),
            Expanded(
              child: Card(
                child: ListView.separated(
                  padding: const EdgeInsets.all(8),
                  itemCount: messages.length,
                  separatorBuilder: (context, index) => const Divider(),
                  itemBuilder: (context, index) {
                    return Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(
                        messages[index],
                        style: Theme.of(context).textTheme.bodyLarge,
                      ),
                    );
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

🙂最後に

Websocketを使った通信はまだ実装したことがなかったので勉強がてらに試してみました。最初はRuby on RailsのWebsocketを使ったAPI作ろうと思ったのですが、Rubyを使った企業から抜けたので、自分の好きなGoでも同じ機能を実装できるのでそっちでやってみました笑
MVCよりコードも少ないしDBも使わないので楽でしたね。

Discussion