Using the web_socket_channel Package to Establish WebSocket Connections in Flutter

Flutter, Google’s UI toolkit, has become a popular choice for building cross-platform applications, offering both performance and a rich developer experience. One of the common requirements in modern apps is real-time communication, which can be achieved using WebSockets. Flutter provides several packages to handle WebSockets, and one of the most reliable and straightforward is web_socket_channel. In this comprehensive guide, we’ll delve into how to use the web_socket_channel package to establish WebSocket connections in Flutter, covering everything from basic setup to advanced usage.

What is WebSocket?

WebSocket is a communication protocol that provides full-duplex communication channels over a single TCP connection. Unlike HTTP, which is request-response based, WebSocket enables real-time data transfer between a client and a server, making it ideal for applications like chat, live feeds, and collaborative tools.

Why Use web_socket_channel in Flutter?

The web_socket_channel package is a simple and reliable way to handle WebSocket connections in Flutter. It offers several benefits:

  • Easy to Use: Simple API for establishing and managing WebSocket connections.
  • Compatibility: Works seamlessly with Flutter’s reactive programming model.
  • Robustness: Provides error handling and reconnection mechanisms.
  • Platform Independent: Supports both web and native Flutter applications.

Setting Up a Flutter Project

Before diving into the code, let’s set up a new Flutter project. If you already have a Flutter project, you can skip this step.

Step 1: Create a New Flutter Project

Open your terminal and run the following command:

flutter create websocket_example
cd websocket_example

Step 2: Add the web_socket_channel Dependency

Add the web_socket_channel package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  web_socket_channel: ^2.4.0 

Then, run flutter pub get to install the package.

Basic Implementation

Now that we have our project set up, let’s start with a basic implementation of a WebSocket connection using the web_socket_channel package.

Step 1: Import the Package

In your Flutter file (e.g., main.dart), import the necessary packages:

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

Step 2: Create a WebSocket Channel

Establish a WebSocket connection by creating a WebSocketChannel instance:

final Uri webSocketUrl = Uri.parse('wss://echo.websocket.events'); // Replace with your WebSocket URL
final WebSocketChannel channel = WebSocketChannel.connect(webSocketUrl);

In this example, we’re using wss://echo.websocket.events, a WebSocket echo service that sends back any message it receives. Replace this URL with your WebSocket server endpoint.

Step 3: Send and Receive Messages

To send messages to the WebSocket server, use the sink.add method:

channel.sink.add('Hello, WebSocket!');

To listen for messages from the server, subscribe to the stream of the WebSocketChannel:

StreamBuilder(
  stream: channel.stream,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return Text('Received: ${snapshot.data}');
    } else if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    } else {
      return Text('Connecting...');
    }
  },
)

Step 4: Close the WebSocket Connection

When you no longer need the WebSocket connection, close it to release resources:

channel.sink.close();

Complete Example

Here’s a complete example integrating these steps into a Flutter application:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'WebSocket Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'WebSocket Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  final TextEditingController _controller = TextEditingController();
  final Uri webSocketUrl = Uri.parse('wss://echo.websocket.events');
  late WebSocketChannel channel;
  String message = '';

  @override
  void initState() {
    super.initState();
    channel = WebSocketChannel.connect(webSocketUrl);

    channel.stream.listen((data) {
      setState(() {
        message = data;
      });
    }, onError: (error) {
      setState(() {
        message = 'Error: $error';
      });
    }, onDone: () {
      setState(() {
        message = 'WebSocket closed';
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Form(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(labelText: 'Send a message'),
              ),
            ),
            const SizedBox(height: 24),
            Text('Received: $message'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _sendMessage,
        tooltip: 'Send message',
        child: const Icon(Icons.send),
      ),
    );
  }

  void _sendMessage() {
    if (_controller.text.isNotEmpty) {
      channel.sink.add(_controller.text);
      _controller.clear();
    }
  }

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

Key components of this example:

  • A TextFormField is used to input messages.
  • The WebSocketChannel is initialized in the initState method and closed in the dispose method to manage the connection lifecycle.
  • The _sendMessage method sends the entered text to the WebSocket server.
  • A listener is attached to channel.stream to update the UI with incoming messages.

Advanced Usage

Now that we’ve covered the basics, let’s look at some advanced techniques for using web_socket_channel.

Handling Errors and Reconnections

WebSocket connections can be unstable due to network issues. It’s important to handle errors and implement reconnection logic.


  @override
  void initState() {
    super.initState();
    connectWebSocket();
  }

  void connectWebSocket() {
    channel = WebSocketChannel.connect(webSocketUrl);

    channel.stream.listen((data) {
      setState(() {
        message = data;
      });
    }, onError: (error) {
      setState(() {
        message = 'Error: $error';
      });
      // Attempt to reconnect after a delay
      Future.delayed(Duration(seconds: 5), () {
        connectWebSocket();
      });
    }, onDone: () {
      setState(() {
        message = 'WebSocket closed';
      });
    });
  }

In this enhanced example, the connectWebSocket method initializes the WebSocket connection and sets up listeners for incoming messages, errors, and connection closing. On encountering an error, the connection attempts to reconnect after a 5-second delay.

Using Streams with Bloc/Provider

To integrate WebSockets with state management solutions like Bloc or Provider, you can use the Stream provided by web_socket_channel.


import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

// WebSocket Event
abstract class WebSocketEvent {}

class WebSocketConnectEvent extends WebSocketEvent {}

class WebSocketMessageEvent extends WebSocketEvent {
  final String message;
  WebSocketMessageEvent(this.message);
}

// WebSocket State
abstract class WebSocketState {
  final String message;
  WebSocketState(this.message);
}

class WebSocketInitialState extends WebSocketState {
  WebSocketInitialState() : super('');
}

class WebSocketConnectedState extends WebSocketState {
  WebSocketConnectedState(String message) : super(message);
}

// WebSocket Bloc
class WebSocketBloc extends Bloc<WebSocketEvent, WebSocketState> {
  final Uri webSocketUrl = Uri.parse('wss://echo.websocket.events');
  late WebSocketChannel channel;

  WebSocketBloc() : super(WebSocketInitialState()) {
    on((event, emit) {
      channel = WebSocketChannel.connect(webSocketUrl);

      channel.stream.listen((data) {
        add(WebSocketMessageEvent(data));
      }, onError: (error) {
        emit(WebSocketConnectedState('Error: $error'));
      }, onDone: () {
        emit(WebSocketConnectedState('WebSocket closed'));
      });

      emit(WebSocketConnectedState('Connecting...'));
    });

    on((event, emit) {
      emit(WebSocketConnectedState(event.message));
    });
  }

  void sendMessage(String message) {
    channel.sink.add(message);
  }

  @override
  Future<void> close() {
    channel.sink.close();
    return super.close();
  }
}

// Usage in UI
class WebSocketScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final webSocketBloc = BlocProvider.of<WebSocketBloc>(context);
    final _controller = TextEditingController();

    return Scaffold(
      appBar: AppBar(
        title: Text('WebSocket with Bloc'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Form(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(labelText: 'Send a message'),
              ),
            ),
            const SizedBox(height: 24),
            BlocBuilder<WebSocketBloc, WebSocketState>(
              builder: (context, state) {
                return Text('Received: ${state.message}');
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.text.isNotEmpty) {
            webSocketBloc.sendMessage(_controller.text);
            _controller.clear();
          }
        },
        tooltip: 'Send message',
        child: const Icon(Icons.send),
      ),
    );
  }
}

Conclusion

Using the web_socket_channel package provides a straightforward and effective way to integrate WebSockets into your Flutter applications. Whether you’re building a chat application, a real-time dashboard, or any other application requiring real-time communication, web_socket_channel offers the necessary tools to manage WebSocket connections efficiently. By handling errors, implementing reconnection logic, and integrating with state management solutions like Bloc or Provider, you can build robust and scalable WebSocket-powered Flutter applications. Understanding these concepts will help you leverage the power of WebSockets to enhance the user experience of your apps.