Hi everyone, in this post I'm going to show you how to use WebSockets in Flutter apps and write a Node backend to test the app.
In this post we’re not going to worry about authentication/authorization, as that was the focus of the previous post in the series. As always, this is meant for people with a decent understanding of the basics of Flutter. I've written a book about Flutter that will get you up to speed reasonably quickly and with ease but, in case you really need to do this now and either don't like learning from the book or don't want to read the whole thing, here's the concepts I'm going to suppose you know how to use in order to follow this post:
- basic structure of a Flutter app (
MaterialApp
,Scaffold
,Column
, definition of custom widgets etc.); - getting input from the user using a
TextField
and managing it with aTextEditingController
; - basic navigation using
Navigator.push
; - asynchronous programming with
Stream
s and the usage of theStreamBuilder
.
WebSocket and Socket.io
This post is about WebSockets. It won't be about Socket.IO, which might be the focus of another post. WebSocket is a protocol (just like HTTP) and there are some packages and libraries to use it directly, but a very popular alternative to doing that is using Socket.io, which is a library that may or may not use WebSocket as its communication protocol, given that it has its own real-time communication engine that is used in case there is no way to establish a WebSocket-based connection.
The way Socket.io does it is rather the other way around, using its own engine to initiate the connection, upgrading to WebSocket if it's possible. This is of particular importance to web developers, whose apps may run on browsers that don't support the WebSocket API, even though this is less and less of a concern as time passes. The main difference you would notice in the example in a tutorial is that Socket.io supports server broadcasting by default, meaning you don't have to manually iterate over the connected clients to send the message to each, as that is a feature of Socket.io itself.
What We're Going to Build
A very common application for WebSockets is building a chat app. That's a very good example but, in my view, it's not a great example for a blog post, unless what one wants to teach is how to build a chat app and not how to use WebSockets. I used the example of a chat app in my book to show how to use Firebase, but that was to show as many aspects of Flutter and Firebase as possible in one example, and it is a cool example.
What I'm going to do in this post, though, is show you everything you need to know in order to build a real time app, leaving the rest to you and avoid showing how to interact with a specific database, how to build a very specific complex user interface: the example is simply going to be an app showing the latest message sent by an user as an announcement to every connected user.
WebSockets in Flutter
The web_socket_channel
Dart WebSocket package is Google-developed and very easy to use. That's what we're going to use in this post.
Opening a Connection
A connection can be opened by creating an object of class WebSocketChannel
, and you can connect to a WebSocket server by using the WebSocketChannel.connect
contructor: channel = WebSocketChannel.connect(URI);
where URI
is an Uri
, that you could get from a String
containing an URL (something like ws://myserver.mydomain.com:3000
) by using Uri.parse(URL)
.
Sending and Receiving Data
The WebSocketChannel.stream
is used to listen for messages. As the name implies, it's a Stream
, which is exactly the best data type for incoming data from a WebSocket. It returns any new messages coming from the WebSocket as soon as they are received.
The WebSocketChannel.sink
is used to send messages. As the name implies, it's a StreamSink
. If you've read my book or have worked with the Firebase Cloud Firestore before, this is used in a similar way to the Firestore
's CollectionReference
object: WebSocketChannel.sink.add(data)
sends data through the WebSocket.
Closing the Connection
If channel
is the WebSocketChannel
, you can close the connection usingchannel.sink.close(statusCode);
. A list of status codes is available in web_socket_channel
's status.dart
:
@override
void dispose() {
super.dispose();
channel.sink.close(statusCodes.goingAway);
}
~~~
## Building an Example App
You can find the complete source code for this app [on this GitHub repository](https://github.com/carzacc/websockets_flutter).
Let's start with the `pubspec.yaml`, which needs to have as a dependency the `web_socket_channel` package:
In lib/main.dart
we're going to import package:web_socket_channel/web_socket_channel.dart
to use the WebSocketChannel
, then we set the server IP and port, and then start an app that has as its home page a class called FauxLoginPage
:
The FauxLoginPage
The FauxLoginPage
is going to be, as the name implies, a fake login page: it's not going to be a proper login page for the user to authenticate, but just a page for the user to set an username. As I wrote above, we're not going to worry about authentication because that was the focus of the previous post. If you know how to use TextField
s (and especially if you're familiar with the Firebase chat app example in my book, which has a login page that works a bit like this one but actually authenticates the user) this is all going to be self-explanatory and simple:
The AnnouncementPage
is going to simply be a StatelessWidget
: we're going to let a StreamBuilder
take care of the changes in values returned by the Stream
of data from the WebSocket. Below the text coming from the WebSocket, we're going to have a TextField
that allows the user to send a message. We convert the data to a string so that it could technically be anything, and be shown to the user as is, to make debugging easier:
The entire main.dart
is going to be the following, then:
Building the Backend for the App
We're going to build the backend for the app with Node.js and the ws
npm package.
The ws Package
There's a very popular and easy-to-use WebSocket client/server package for Node called simply ws, so you can install it using
$ npm install ws
You can start a WebSocket server that listens to a given port with the following code:{% raw %}
var server = new WebSocket.Server(
{
port: port,
}
);
you can wait for a connection and define a callback to be ran when a client connects with the following code:{% raw %}
server.on('connection', function connection(client) {
// code to execute when a client connects
});
This gives us a {% raw %}`client` object we can use to send messages to the connected WebSocket client using `client.send()`:
client.send(msg);
We can also listen for messages sent by the client over the WebSocket and run a function when that happens:{% raw %}
client.on('message', function incoming(message) {
// code to execute when a message is received
});
An useful member of the {% raw %}`server` object is `server.clients`, which is an array of the connected clients. This means we can send a message to each connected client with the following:
for(var cl of server.clients) {
cl.send(message);
}
### Implementing the Backend
You can find source code for the backend at [this GitHub repository](https://github.com/carzacc/websocketsbackend).
The first thing we need is to import the *ws* package:
{% gist https://gist.github.com/carzacc/430d50943715dfbde2bd2082992af9a3 file=imports.js %}
Then set a port and start the WebSocket server:
{% gist https://gist.github.com/carzacc/430d50943715dfbde2bd2082992af9a3 file=startserver.js %}
let's also define a default message to send to the client the first time:
{% gist https://gist.github.com/carzacc/430d50943715dfbde2bd2082992af9a3 file=letmsg.js %}
When a client connects for the first time, we send them that message so the client has something to display to the user when they connect:
{% gist https://gist.github.com/carzacc/430d50943715dfbde2bd2082992af9a3 file=connection.js %}
Now, let's handle the reception of a message:
{% gist https://gist.github.com/carzacc/430d50943715dfbde2bd2082992af9a3 file=onmessage.js %}
What we should do is broadcast the received message to all connected clients:
{% gist https://gist.github.com/carzacc/430d50943715dfbde2bd2082992af9a3 file=broadcast.js %}
If we also log to console each received message the final {% raw %}`index.js` is the following:
As always, if you enjoyed this post, consider following me on Twitter @carminezacc.
Top comments (3)
WHERE ARE U INITIALIZE SERVER?
Hmm, you're right, that gist isn't right. Check out the post on my blog carmine.dev/posts/flutterwebsockets/ which doesn't have the mistake, I'll fix it here ASAP
Can we send any kind of data through the websocket channel ?