Gonçalo Palma Lead Flutter Developer at PvotalTech, writer.

Using WebSockets in Flutter

7 min read 2211

Using WebSockets In Flutter

Nowadays, many applications need real-time data to provide instant feedback to users, be it a chat application that shows a person typing in real-time or a remote application that plots data directly from a hardware sensor.

We try to solve these problems with REST but face a complex problem: to have near-instant feedback, we must ping the server several times per minute, which can be architecturally difficult to achieve and it overloads the server.

However, if using solutions like Firebase Realtime Database, you can notice that as soon as a new record adds to the database, the Flutter application receives it as a Stream and you show the data to the user.

But how does Firebase achieve this? The reality is, frameworks such as Firebase and others rely on a very important piece of technology: WebSockets.

In this article, we’re going to explore how we can use our own WebSockets to create applications that display real-time data to our users.

What is the WebSocket API?

According to Mozilla, the WebSocket API is “an advanced technology that makes it possible to open a two-way interactive communication session between the user’s browser and a server… you can send messages to a server and receive event-driven responses without having to poll the server for a reply.”

In a nutshell, WebSockets are composed of:

  • A server that streams information
  • A client in the application that is ready to receive the new stream of data
  • A channel to communicate between the client and the server
  • Messages sent between the client and the server

As opposed to REST, with WebSockets, we don’t await a response from the server after we send a message to it. We can send one message and receive dozens of incoming messages from the server.

In a way, you can think of it as subscribing to notifications; we subscribe to a specific topic, for example, the exchange value of USD-EUR, and then we receive a new message from the server each time that the USD-EUR exchange value changed.

This real-time communication stream makes WebSockets the ideal technology to use for stock-exchange apps, chat applications, IoT apps, and every other app that must receive an incoming stream of data.

We made a custom demo for .
No really. Click here to check it out.

The question is, how can we use them in Flutter?

Using WebSockets in Dart

Fortunately, Flutter’s language, Dart, provides us with an out-of-box solution for dealing with WebSockets: the WebSocket class.

If we develop apps for only one target (either desktop or web or mobile), then we can use WebSocket safely.

However, the moment we opt to use our app across platforms, we must stay aware that the WebSocket class relies on dart:io and dart:html, which means that we cannot compile for both mobile and web at the same time.

Thankfully, the Dart team created the web_socket_channel, an official library that abstracts the dart:io and dart:html logic and allows us to use one class to create a multiplatform application.

To use web_spclet_channel, we must follow three simple steps:

  1. Create a new client with WebSocketChannel and connect to a channel via the connect function
  2. Listen to incoming messages with the stream getter
  3. Use the sink getter to send messages to the server

For this current article, we will use an example from the CoinBase Pro API to get the real-time value of cryptocurrencies via WebSockets.

Creating a new Dart project

Let’s start by creating a brand new Dart project by using the following on the command line:

dart create websocket_example

This creates a new Dart-only project in the websocket_example folder.

Next, open the project in your favorite IDE and add the web_socket_channel dependency:

dependencies:
  web_socket_channel: ^2.1.0

With the project created, let’s look at the CoinBase Pro API.

Our WebSocket client must connect to the URI, wss://ws-feed.pro.coinbase.com, to receive updates. To listen to those updates, we must listen for any incoming messages:

import 'package:web_socket_channel/web_socket_channel.dart';

void main(List<String> arguments) {
  /// Create the WebSocket channel
  final channel = WebSocketChannel.connect(
    Uri.parse('wss://ws-feed.pro.coinbase.com'),
  );

  /// Listen for all incoming data
  channel.stream.listen(
    (data) {
      print(data);
    },
    onError: (error) => print(error),
  );

}

Since we want to receive updates from the server with the current value of BTC-EUR, we must use the subscribe message from the API. To send a message to the server, we can use sink from our newly created WebSocketChannel to send a JSON message to the server:

import 'dart:convert';

import 'package:web_socket_channel/web_socket_channel.dart';

void main(List<String> arguments) async {
  /// Create the WebSocket channel
  final channel = WebSocketChannel.connect(
    Uri.parse('wss://ws-feed.pro.coinbase.com'),
  );

  channel.sink.add(
    jsonEncode(
      {
        "type": "subscribe",
        "channels": [
          {
            "name": "ticker",
            "product_ids": [
              "BTC-EUR",
            ]
          }
        ]
      },
    ),
  );

  /// Listen for all incoming data
  channel.stream.listen(
    (data) {
      print(data);
    },
    onError: (error) => print(error),
  );
}

To see it in action, we can use the dart run bin/web_socket_example.dart command. Hopefully, we can see new logs printed with incoming messages from the CoinBase Pro API:

{"type":"subscriptions","channels":[{"name":"ticker","product_ids":["BTC-EUR"]}]}
{"type":"ticker","sequence":12764748963,"product_id":"BTC-EUR","price":"36941.64","open_24h":"38285.55","volume_24h":"4722.06983151","low_24h":"34300","high_24h":"38674.98","volume_30d":"43802.53509763","best_bid":"36934.04","best_ask":"36941.64","side":"buy","time":"2021-09-21T08:00:20.424250Z","trade_id":50228771,"last_size":"0.08072577"}
{"type":"ticker","sequence":12764749180,"product_id":"BTC-EUR","price":"36941.55","open_24h":"38285.55","volume_24h":"4722.08309939","low_24h":"34300","high_24h":"38674.98","volume_30d":"43802.54836551","best_bid":"36933.09","best_ask":"36941.55","side":"buy","time":"2021-09-21T08:00:21.167335Z","trade_id":50228772,"last_size":"0.01326788"}

One question remains: we can open a channel, but how can we close it?

Closing the channel

The same way we access the underlying sink to send new data to the server, we can also use it to close the connection after a period of time:

void main(List<String> arguments) async {
  // ...

  /// Wait for 5 seconds
  await Future.delayed(Duration(seconds: 5));

  /// Close the channel
  channel.sink.close();
}

If we rerun the app, we see that after five seconds, it closes the channel and the application.

Displaying real-time data in Flutter

Now that we created a simple Dart application that uses WebSockets, we can create a Flutter app to showcase how we can use WebSockets. Thankfully, all the code we’ve done previously can be reused in our Flutter application. This app will:

  • Show values for ETH-EUR
  • Show values for BTC-EUR
  • Close both channels if the user wants

For this purpose, we’ll use two different socket channels: one that receives the ETH-EUR messages and another that receives the BTC-EUR messages.

However, keep in mind that the CoinBase Pro API lets us use one single channel to receive multiple values, which can use and map the incoming messages to display in different widgets.

Instead of using plain Maps to receive and send messages to the server, we must create the appropriate CoinbaseRequest and CoinbaseResponse classes and use json_serializable to easily serialize and deserialize them:

import 'package:json_annotation/json_annotation.dart';

part 'coinbase_request.g.dart';

@JsonSerializable()
class CoinbaseRequest {
  final String type;
  final List<Map<String, dynamic>> channels;

  CoinbaseRequest(this.type, this.channels);

  factory CoinbaseRequest.fromJson(Map<String, dynamic> json) =>
      _$CoinbaseRequestFromJson(json);

  Map<String, dynamic> toJson() => _$CoinbaseRequestToJson(this);
}
import 'package:json_annotation/json_annotation.dart';

part 'coinbase_response.g.dart';

@JsonSerializable()
class CoinbaseResponse {
  final String price;
  @JsonKey(name: 'product_id')
  final String productId;

  CoinbaseResponse(this.price, this.productId);

  factory CoinbaseResponse.fromJson(Map<String, dynamic> json) =>
      _$CoinbaseResponseFromJson(json);

  Map<String, dynamic> toJson() => _$CoinbaseResponseToJson(this);
}

To isolate our WebSocket logic, let’s create CoinbaseProvider that is responsible for:

  • Creating the WebSocket channel via the constructor
  • Sending messages to the channel with the openBitcoin method
  • Exposing a Stream of data using bitcoinStream
  • Closing the WebSocket channel with the closeBitcoin method

The same applies to Ethereum, but to make the example more readable, we’ll present only the code for BTC-EUR:

/// Manages opening and closing the sockets for the
/// two different sockets: ETH socker and BTC socket
class CoinbaseProvider {
  late final WebSocketChannel _btcWebsocket;

  static const _btcProduct = "BTC-EUR";

  CoinbaseProvider()
      : _btcWebsocket = WebSocketChannel.connect(
          Uri.parse('wss://ws-feed.pro.coinbase.com'),
        );

  Stream<CoinbaseResponse> get bitcoinStream => _btcWebsocket.stream
      .map<CoinbaseResponse>(
          (value) => CoinbaseResponse.fromJson(jsonDecode(value)))
      .skipWhile((element) => element.productId != _btcProduct);

  void openBitcoin() {
    _btcWebsocket.sink.add(
      jsonEncode(
        CoinbaseRequest(
          'subscribe',
          [
            {
              "name": "ticker",
              "product_ids": [_btcProduct]
            }
          ],
        ).toJson(),
      ),
    );
  }

  void closeBitcoin() {
    _btcWebsocket.sink.close();
  }

Using the StreamBuilder widget

From our previous experience, we discovered that WebSockets in Dart expose a Stream to receive incoming messages from the server, which means that we need a widget that can receive a Stream and react to it.

For that, we can use the StreamBuilder widget, which tells us if:

  • We are waiting for data
  • We received data
  • We received an error
  • The stream closed with a done event

Using our provider, we can create a widget that displays these states visually, such as a spinner to indicate we are waiting for data.

Loading Spinner

We can show our received data by displaying the current price for both ETH-EUR and BTC-EUR.

Displaying Coin Brice, Showing Etherium Price And Bitcoin Price

And, we can close the WebSockets and display, “No more data.”

Showing "No more data"

With the StreamBuilder connectionState and data to get information regarding each state, we can replicate all the three states we want to show to our user:

  • Loading state for when we haven’t received any data yet
  • Current ETH-EUR or BTC-EUR price
  • A closed state after we close the WebSocket connection
class CoinPrice extends StatelessWidget {
  final Stream stream;
  final Color color;

  const CoinPrice({
    required this.stream,
    required this.color,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(12.0),
      child: StreamBuilder(
        stream: stream,
        builder: (context, snapshot) {
          /// We are waiting for incoming data data
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }

          /// We have an active connection and we have received data
          if (snapshot.connectionState == ConnectionState.active &&
              snapshot.hasData) {
            return Center(
              child: Text(
                '${snapshot.data!.productId}: ${snapshot.data!.price}',
                style: TextStyle(
                  color: color,
                  fontSize: 24.0,
                  fontWeight: FontWeight.bold,
                ),
              ),
            );
          }

          /// When we have closed the connection
          if (snapshot.connectionState == ConnectionState.done) {
            return const Center(
              child: Text(
                'No more data',
                style: TextStyle(
                  color: Colors.red,
                ),
              ),
            );
          }

          /// For all other situations, we display a simple "No data"
          /// message
          return const Center(
            child: Text('No data'),
          );
        },
      ),
    );
  }
}

Creating the button widgets

With our widget that receives the stream of completed data, we can proceed to create a button widget that sends a message to the CoinBase server to receive information about the current price of BTC-EUR and another button that closes all socket connections.

The first button will be the CoinButton, which can send a message via the WebSocket to start receiving data about a specific coin.

Instead of creating two different widgets, one for each cryptocurrency, we’ll create a generic widget that accepts title, backgroundColor, and isOpen to know if we’re already receiving information for that coin.

And finally, it will accept onTap that allows us to call the CoinbaseProvider with the appropriate function:

/// Send WebSocket message to receive information
/// regarding a certain coin
/// 
/// Usage:
/// ```dart
/// CoinButton(
///  isSocketOpen: _showBitcoin,
///  title: "BTC",
///  backgroundColor: Colors.orange,
///  onTap: (isSocketOpen) {
///    if (!isSocketOpen) {
///      widget.provider.openBitcoin();
///      setState(() => _showBitcoin = true);
///    }
///  },
///)
/// ```
class CoinButton extends StatelessWidget {
  final bool isSocketOpen;
  final String title;
  final Function(bool) onTap;
  final Color backgroundColor;

  const CoinButton({
    required this.isSocketOpen,
    required this.title,
    required this.onTap,
    required this.backgroundColor,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialButton(
      onPressed: () => !isSocketOpen ? onTap(isSocketOpen) : null,
      color: backgroundColor,
      child: Row(
        children: [
          if (!isSocketOpen)
            const Icon(
              Icons.close,
              color: Colors.red,
            ),
          if (isSocketOpen)
            const Icon(
              Icons.check,
              color: Colors.green,
            ),
          Text(title),
        ],
      ),
    );
  }
}

Secondly, we’ll create a CloseWebSocketsWidget that accepts the CoinbaseProvider as an argument so we can access the closeBitcoin and closeEthereum functions:

/// Button to close the WebSocket connection
class CloseSocketsWidget extends StatelessWidget {
  const CloseSocketsWidget({
    Key? key,
    required this.provider,

  }) : super(key: key);

  final CoinbaseProvider provider;

  @override
  Widget build(BuildContext context) {
    return MaterialButton(
      color: Colors.red,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: const <Widget>[
          Icon(
            Icons.warning,
            color: Colors.white,
          ),
          SizedBox(
            width: 12.0,
          ),
          Text(
            "CLOSE SOCKETS",
            style: TextStyle(
              color: Colors.white,
            ),
          )
        ],
      ),
      onPressed: () {
        provider.closeBitcoin();
        provider.closeEthereum();
      },
    );
  }
}

Putting it all together, we can create a simple app that shows different cryptocurrency prices in real-time:

Conclusion

There will be times when we must display real-time data to a user, and as we have seen, WebSockets can make the process easy for us in Flutter applications with just four simple steps:

  1. Creating a client
  2. Connecting the client to a channel
  3. Sending messages to the server
  4. Listening for incoming messages

This allows us to create reactive applications that our StreamBuilders widget can change according to the current state. Are we waiting for new data? Did we receive an error or new data?

These questions can be translated into UI components, such as the animation indicating that someone is typing in a chat or updating the value of a graph on a page.

So now, a question for you! Have you ever needed to display real-time data to users in your applications? If so, what did you use? Firebase? WebSockets? gRPC server-side Stream?

I’m very curious to hear back from you! 😁

To see the full code you can check out this repository.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Gonçalo Palma Lead Flutter Developer at PvotalTech, writer.

Leave a Reply