Handling Different WebSocket Events (Open, Message, Close, Error) in Flutter

WebSockets provide a persistent connection between a client and a server, enabling real-time data transfer. In Flutter, handling different WebSocket events such as open, message, close, and error is crucial for building robust and interactive applications. This article delves into how to manage these events effectively in Flutter.

Understanding WebSocket Events

WebSockets involve several key events that developers need to handle:

  • Open: Signifies the establishment of a WebSocket connection.
  • Message: Indicates the reception of data from the server.
  • Close: Denotes the termination of the WebSocket connection.
  • Error: Signals an error condition during the WebSocket lifecycle.

Setting Up the WebSocket Connection in Flutter

Before handling events, establish a WebSocket connection using the web_socket_channel package.

Step 1: Add Dependency

Add the web_socket_channel package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  web_socket_channel: ^2.4.0

Run flutter pub get to install the dependency.

Step 2: Establish WebSocket Connection

Here’s how to establish a WebSocket connection:

import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:flutter/material.dart';

class WebSocketHandler {
  final String url;
  WebSocketChannel? channel;

  WebSocketHandler(this.url);

  void connect() {
    try {
      channel = WebSocketChannel.connect(Uri.parse(url));
    } catch (e) {
      print('Error connecting to WebSocket: $e');
    }
  }

  void disconnect() {
    channel?.sink.close();
  }
}

Handling the Open Event

The WebSocket “open” event doesn’t have a direct callback in the web_socket_channel package. Connection establishment is managed internally. Successful connection implies the “open” event has occurred. You can display a message or perform initialization tasks upon successful connection.

Handling the Message Event

Listen for incoming messages from the WebSocket connection. This is critical for real-time data processing.

class WebSocketHandler {
  //... previous code ...

  Stream getMessageStream() {
    return channel?.stream ?? Stream.empty();
  }
}

class MyHomePage extends StatefulWidget {
  final WebSocketHandler socketHandler;

  MyHomePage({required this.socketHandler});

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  @override
  void initState() {
    super.initState();
    widget.socketHandler.connect();
  }

  @override
  void dispose() {
    widget.socketHandler.disconnect();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('WebSocket Example'),
      ),
      body: StreamBuilder(
        stream: widget.socketHandler.getMessageStream(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return Center(
              child: Text('Received: ${snapshot.data}'),
            );
          } else if (snapshot.hasError) {
            return Center(
              child: Text('Error: ${snapshot.error}'),
            );
          } else {
            return Center(
              child: CircularProgressIndicator(),
            );
          }
        },
      ),
    );
  }
}

Use StreamBuilder to listen to the WebSocket stream. Upon receiving a message, update the UI accordingly.

Handling the Close Event

Detect when the WebSocket connection is closed. Proper handling can trigger reconnection attempts or UI updates.

class WebSocketHandler {
  //... previous code ...

  void listenForCloseEvent(VoidCallback onClose) {
    channel?.sink.done.then((_) {
      onClose();
    });
  }
}

class _MyHomePageState extends State {
  bool isConnected = false;

  @override
  void initState() {
    super.initState();
    widget.socketHandler.connect();
    isConnected = true;

    widget.socketHandler.listenForCloseEvent(() {
      setState(() {
        isConnected = false;
      });
      print('WebSocket connection closed.');
      // Implement reconnection logic here
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('WebSocket Example'),
      ),
      body: Column(
        children: [
          if (!isConnected)
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text('Disconnected from WebSocket'),
            ),
          Expanded(
            child: StreamBuilder(
              stream: widget.socketHandler.getMessageStream(),
              builder: (context, snapshot) {
                //... message handling code ...
              },
            ),
          ),
        ],
      ),
    );
  }
}

Use channel.sink.done to listen for the connection close event. The callback triggers when the WebSocket is closed.

Handling the Error Event

Handle errors during the WebSocket lifecycle. Errors may arise due to network issues, server problems, or protocol violations.

class WebSocketHandler {
  //... previous code ...

  Stream getMessageStream() {
    return channel?.stream.handleError((error) {
      print('WebSocket error: $error');
      // Handle error, e.g., by showing an error message
    }) ?? Stream.empty();
  }
}

class _MyHomePageState extends State {
  //... previous code ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('WebSocket Example'),
      ),
      body: Column(
        children: [
          if (!isConnected)
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text('Disconnected from WebSocket'),
            ),
          Expanded(
            child: StreamBuilder(
              stream: widget.socketHandler.getMessageStream(),
              builder: (context, snapshot) {
                if (snapshot.hasError) {
                  return Center(
                    child: Text('Error: ${snapshot.error}'),
                  );
                }
                //... message handling code ...
              },
            ),
          ),
        ],
      ),
    );
  }
}

Use stream.handleError to catch errors. Handle these errors by logging them and informing the user.

Complete Example

Here is a complete example that ties all the concepts together:


import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

class WebSocketHandler {
  final String url;
  WebSocketChannel? channel;
  bool isConnected = false;

  WebSocketHandler(this.url);

  void connect() {
    try {
      channel = WebSocketChannel.connect(Uri.parse(url));
      isConnected = true;
    } catch (e) {
      isConnected = false;
      print('Error connecting to WebSocket: $e');
    }
  }

  void disconnect() {
    channel?.sink.close();
    isConnected = false;
  }

  Stream getMessageStream() {
    return channel?.stream.handleError((error) {
      print('WebSocket error: $error');
      isConnected = false;
    }) ?? Stream.empty();
  }

  void listenForCloseEvent(VoidCallback onClose) {
    channel?.sink.done.then((_) {
      isConnected = false;
      onClose();
    });
  }
}

class MyHomePage extends StatefulWidget {
  final WebSocketHandler socketHandler;

  MyHomePage({required this.socketHandler});

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  @override
  void initState() {
    super.initState();
    widget.socketHandler.connect();
    widget.socketHandler.listenForCloseEvent(() {
      setState(() {});
      print('WebSocket connection closed.');
    });
  }

  @override
  void dispose() {
    widget.socketHandler.disconnect();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('WebSocket Example'),
      ),
      body: Column(
        children: [
          if (!widget.socketHandler.isConnected)
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text('Disconnected from WebSocket'),
            ),
          Expanded(
            child: StreamBuilder(
              stream: widget.socketHandler.getMessageStream(),
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return Center(
                    child: Text('Received: ${snapshot.data}'),
                  );
                } else if (snapshot.hasError) {
                  return Center(
                    child: Text('Error: ${snapshot.error}'),
                  );
                } else {
                  return Center(
                    child: CircularProgressIndicator(),
                  );
                }
              },
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(
    MaterialApp(
      home: MyHomePage(
        socketHandler: WebSocketHandler('wss://echo.websocket.events'),
      ),
    ),
  );
}

Conclusion

Properly handling WebSocket events (open, message, close, error) is essential for building reliable real-time applications in Flutter. Using the web_socket_channel package along with StreamBuilder and error handling, you can create a robust WebSocket client capable of managing various connection states and errors. The provided examples illustrate how to implement these features, ensuring your application can handle WebSocket events effectively.