WebSockets provide a powerful way to establish a full-duplex communication channel between the client and server, allowing real-time data exchange. This is particularly useful in applications that require live updates, such as chat apps, online gaming, financial dashboards, and collaborative tools. In this blog post, we’ll explore how to effectively implement WebSockets in Flutter, using a robust and well-structured approach.
Why WebSockets?
Imagine you’re building a chat app. You want messages to appear instantly without the user having to refresh the page or wait for updates. WebSockets make this happen by keeping a continuous connection open between your app and the server. This allows data to flow back and forth instantly, giving your users that smooth, real-time experience.
Setting Up WebSockets in Flutter
To get started with WebSockets in Flutter, you first need to import the necessary Dart packages in pubspec.yaml:
dependencies:
retry: ^3.1.0
Meet the WebSocket Manager
A well-structured approach to handling WebSocket connections in Flutter is to use a singleton class. This class will handle everything related to WebSockets, from connecting to the server to managing incoming data.
import 'dart:async';
import 'dart:developer';
import 'dart:io';
class WebSocketManager {
static final WebSocketManager _singleton = WebSocketManager._internal();
final StreamController<Object> streamController = StreamController.broadcast(sync: true);
late String wsUrl;
WebSocket? socket;
WebSocketStatus status = WebSocketStatus.closed;
factory WebSocketManager(String url) {
_singleton.wsUrl = url;
return _singleton;
}
WebSocketManager._internal();
Future<void> initWebSocketConnection() async {
if (status == WebSocketStatus.closed) {
status = WebSocketStatus.connecting;
log("Socket Connecting...");
socket = await connectWs();
if (socket?.readyState == WebSocket.open) {
socket!.pingInterval = const Duration(seconds: 5);
status = WebSocketStatus.open;
log("Connection OPEN");
broadcast();
} else {
log("Connection DONE inside initWebSocketConnection()");
log("Close Code: ${socket?.closeCode}");
status = WebSocketStatus.closed;
streamController.addError('Connection Done with close code: ${socket?.closeCode}');
}
}
}
void broadcast() {
socket?.listen(
(dynamic streamData) {
try {
streamController.add(streamData as String);
} catch (e) {
streamController.addError(e.toString());
}
},
onDone: () {
status = WebSocketStatus.closed;
log("Connection DONE");
streamController.addError('Connection Done with close code: ${socket?.closeCode}');
},
onError: (dynamic e) async {
status = WebSocketStatus.closed;
streamController.addError(e.toString());
},
);
}
Future<WebSocket?> connectWs() async {
const r = RetryOptions(maxAttempts: 20, delayFactor: Duration(seconds: 1));
WebSocket? response;
try {
response = await r.retry(
() => WebSocket.connect(wsUrl),
retryIf: (e) {
return true;
},
onRetry: (e) {
log("Socket Connection Error: $e");
log("Retrying!");
},
);
} on TimeoutException {
log("Socket TimeoutException!");
initWebSocketConnection();
return null;
} catch (e) {
log("Socket connection error: $e");
return null;
}
return response;
}
Future<void> dispose() async {
await socket?.close();
socket = null;
status = WebSocketStatus.closed;
}
}
Key Components of the WebSocketManager Class
Singleton Pattern: The class uses a singleton pattern to ensure only one instance of WebSocketManager exists, managing the WebSocket connection across the entire app.
StreamController: This is used to broadcast incoming messages from the WebSocket to multiple listeners within the app.
WebSocket Status: The WebSocketStatus enum tracks the state of the WebSocket connection (open, closed, or connecting).
Retry Logic: Exponential backoff with jitter is implemented using the retry package, allowing the WebSocket to automatically retry the connection multiple times if it fails.
Adding WebSocketManager to Your Flutter App
Once you’ve set up the WebSocketManager class, the next step is to integrate it into your Flutter application. Below is an example of how to use the WebSocketManager in your app:
wsManager = WebSocketManager("https://echo.websocket.org/");
wsManager?.initWebSocketConnection();
final streamController = wsManager?.streamController;
StreamBuilder<dynamic>(
stream: streamController?.stream,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasData) {
return Text('Received data: ${snapshot.data}');
}
return const Text('No data available');
},
)
What’s Going On Here?
WebSocket Initialization: We start by creating our WebSocketManager with a URL. This URL tells the WebSocket where to connect.
Connection Setup: Next, we call initWebSocketConnection() to start the connection. This gets our WebSocket up and running.
StreamController: Remember that radio broadcaster? We’re tuning into it here. We grab the StreamController from our WebSocketManager to listen to any messages coming through.
StreamBuilder: This function builds the UI every time a new snapshot of data arrives on the stream. It also runs when the stream’s connection state changes.
This approach ensures that your UI stays responsive and up-to-date with real-time data from the WebSocket.
Conclusion
WebSockets in Flutter might sound a bit intimidating at first, but with a solid understanding of how to manage connections and data, you’ll be up and running in no time. Remember, the key is to keep it simple, handle errors gracefully, and enjoy the process of bringing real-time communication to life in your app!
Happy coding! 🎉