The channel stream is easier to understand on video, where each connection and close event is visible.
Choose a socket instead of HTTP polling
A WebSocket keeps one bidirectional connection, whereas polling creates a fresh HTTP request at fixed intervals. Use sockets for chat, collaborative presence, live prices, or telemetry where low-latency server pushes matter.
For infrequent updates, ordinary HTTP is simpler to cache, secure, observe, and retry.
Open a WebSocketChannel connection
web_socket_channel gives mobile, desktop, and web code the same WebSocketChannel interface. Call WebSocketChannel.connect with a ws or wss URI and await channel.ready before enabling controls that send data.
A connection attempt can fail before a stream event arrives, so surface ready errors as a distinct connecting state.
flutter pub add web_socket_channel
Instagram sketches break a reconnect loop into socket, timer, token, and subscription ownership.
Render channel.stream with StreamBuilder
The channel stream emits each server frame and fits naturally into StreamBuilder for a small screen. Handle waiting, error, active data, and done states, decoding only frames whose format the protocol permits.
Creating the channel inside build opens a new socket on every rebuild and leaks the earlier connection.

Build production-ready WebSockets features
The Complete Flutter Guide turns websockets into maintainable app architecture, polished UI, and testable production code.
Enrol nowDeclare web_socket_channel in pubspec.yaml so the same channel interface is available on Flutter mobile and web.
dependencies:
flutter:
sdk: flutter
web_socket_channel: any
Send frames through channel.sink
channel.sink.add writes a text or byte frame on the existing connection. Validate empty input, encode the protocol message, and clear the TextEditingController only after add has been attempted.
Do not assume sink.add confirms delivery; application-level acknowledgements are needed when delivery matters.
import 'package:web_socket_channel/web_socket_channel.dart';
Put JSON on the wire deliberately
jsonEncode turns a typed command into a text frame and jsonDecode reverses a received text frame. Include a type field and stable payload keys so the receiver can dispatch messages without guessing from their shape.
Malformed frames raise FormatException, and unexpected field types should be rejected instead of cast blindly.
import 'dart:convert';
import 'package:web_socket_channel/web_socket_channel.dart';
final channel = WebSocketChannel.connect(
Uri.parse('wss://echo.websocket.events'),
);
void sendMessage(String text) {
channel.sink.add(jsonEncode({'type': 'message', 'text': text}));
}
Stream<Map<String, dynamic>> get messages => channel.stream.map(
(value) => jsonDecode(value as String) as Map<String, dynamic>,
);
Future<void> close() => channel.sink.close();
A longer LinkedIn discussion explores backoff, idempotency, and message ordering under real network changes.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowClose sockets with a meaningful status
Close the sink in dispose so navigation does not leave a server session and listener alive. Use status.goingAway for intentional screen departure and reserve normalClosure for a completed protocol conversation.
Calling setState from a late stream callback after disposal produces lifecycle errors unless the subscription has ended.
Widget buildMessages() {
return StreamBuilder<dynamic>(
stream: _channel.stream,
builder: (context, snapshot) {
if (snapshot.hasError) return Text('Socket failed: ${snapshot.error}');
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (!snapshot.hasData) return const Text('Connection closed');
final message = jsonDecode(snapshot.data as String) as Map<String, dynamic>;
return Text(message['text'] as String? ?? 'Unknown message');
},
);
}
void sendChat(String text) {
_channel.sink.add(jsonEncode({'type': 'chat', 'text': text.trim()}));
}
Reconnect with capped backoff
onDone and onError can schedule a new channel after one, two, four, then at most thirty seconds. Reset the delay after a healthy connection and cancel the pending Timer when the screen is disposed.
Immediate recursive reconnects hammer an offline network and can create several simultaneous sockets.
void scheduleReconnect() {
if (!mounted || _reconnectTimer?.isActive == true) return;
final wait = Duration(seconds: math.min(30, 1 << _attempt));
_reconnectTimer = Timer(wait, () {
_attempt++;
connect();
});
}
@override
void dispose() {
_reconnectTimer?.cancel();
_channel.sink.close(status.goingAway, 'Screen closed');
super.dispose();
}
Exercise the connection under pressure
A socket that works on a desk can still fail badly after a train enters a tunnel or a phone sleeps. Test the client by switching between Wi-Fi and mobile data, backgrounding the app, and briefly disabling connectivity. Each interruption should produce an explicit state such as connecting, live, retrying, or offline. The interface can then distinguish a delayed stream from an empty stream instead of showing stale values as though they were current.
Reconnect logic needs a ceiling and some randomness. Exponential backoff reduces load when many devices lose the service together, while jitter prevents them from reconnecting on the same millisecond. Reset the attempt counter only after the connection has remained healthy long enough to count as recovered. A socket that opens and immediately closes should continue backing off. Keep the timer owned by the same object as the channel, and cancel both when the feature is disposed or the user signs out.
Message delivery also requires a written contract. If the protocol can replay events, attach a stable event ID and ignore IDs already applied. If ordering matters, include a sequence number and request a snapshot when a gap appears. JSON decoding should happen behind a boundary that can reject an unknown message type without crashing the stream listener. Record the type and correlation ID for diagnostics, but avoid logging tokens, chat text, or other private payload fields.
Authentication expiry is easiest to handle before it becomes a reconnect loop. When the server rejects a stale credential, pause new connection attempts, refresh the token through the normal authentication layer, and create a fresh channel with the replacement value. Verify the behaviour with a short-lived test token and a server restart. Finally, confirm that only one subscription updates the UI after every recovery; duplicate listeners often reveal themselves as doubled counters or repeated notifications rather than an obvious exception.
Close-handshake behaviour belongs in the protocol tests too. Distinguish an intentional server close from a transport loss, retain any close code and safe reason, and decide which codes permit reconnection. Outgoing messages need a policy while disconnected: reject them, queue a bounded set, or persist commands with idempotency IDs. An unlimited memory queue turns a long outage into a burst that freezes the client and overloads the recovered service. Use a small integration server to send invalid JSON, fragmented timing, heartbeats, ordered events, and chosen close codes. That harness verifies the Dart stream and channel behaviour without relying on a public echo endpoint whose availability and protocol are outside the project’s control.
Common mistakes
- Choose a socket instead of HTTP polling: In WebSocket messaging, for infrequent updates, ordinary HTTP is simpler to cache, secure, observe, and retry; inspect this WebSocket messaging cause before changing another WebSocket messaging widget.
- Open a WebSocketChannel connection: In WebSocket messaging, a connection attempt can fail before a stream event arrives, so surface ready errors as a distinct connecting state; inspect this WebSocket messaging cause before changing another WebSocket messaging widget.
- Render channel.stream with StreamBuilder: In WebSocket messaging, creating the channel inside build opens a new socket on every rebuild and leaks the earlier connection; inspect this WebSocket messaging cause before changing another WebSocket messaging widget.
- Send frames through channel.sink: In WebSocket messaging, do not assume sink.add confirms delivery; application-level acknowledgements are needed when delivery matters; inspect this WebSocket messaging cause before changing another WebSocket messaging widget.
- Put JSON on the wire deliberately: In WebSocket messaging, malformed frames raise FormatException, and unexpected field types should be rejected instead of cast blindly; inspect this WebSocket messaging cause before changing another WebSocket messaging widget.
- Close sockets with a meaningful status: In WebSocket messaging, calling setState from a late stream callback after disposal produces lifecycle errors unless the subscription has ended; inspect this WebSocket messaging cause before changing another WebSocket messaging widget.
Frequently asked questions
How does choose a socket instead of HTTP polling work in WebSocket messaging?
For WebSocket messaging, the starting rule is that a WebSocket keeps one bidirectional connection, whereas polling creates a fresh HTTP request at fixed intervals. Apply this WebSocket messaging rule first because choose a socket instead of HTTP polling determines whether the WebSocket messaging pattern fits.
Why does render channel.stream with StreamBuilder matter for WebSocket messaging?
In WebSocket messaging, handle waiting, error, active data, and done states, decoding only frames whose format the protocol permits. Keeping render channel.stream with StreamBuilder at the WebSocket messaging call site exposes the WebSocket messaging return value directly.
What failure should I test first in WebSocket messaging?
First reproduce the WebSocket messaging case where malformed frames raise FormatException, and unexpected field types should be rejected instead of cast blindly. The successful path serialises the intended JSON shape and rejects a malformed frame without closing the screen.
How can I verify WebSocket messaging before release?
Exercise reconnect with capped backoff with real WebSocket messaging inputs on every shipped platform. Inspect the final WebSocket messaging callback or output; a successful WebSocket messaging button tap alone is not proof.
Further reads
Connect WebSocket messaging to the surrounding Flutter stack through these published tutorials:
- Flutter Development Guide 2026
- Flutter HTTP Requests: GET, POST, and JSON With the http Package
- Flutter FutureBuilder vs StreamBuilder: Render Async Data Safely
- Flutter Handle API Errors: Try/Catch, Retries, and User Feedback
- Flutter REST API Integration: Fetch, Display, and Refresh Data
Sources: Flutter framework and Dart API documentation; WebSocket messaging examples verified against current stable Flutter.