WebSockets allow real-time communication between the Mobile App and the Server. You will learn the principles through a concrete example of a basic real-time multiplayers game.

Difficulty: Intermediate

Forewords

Unless you are building an application that does not need to exchange information with a server, communication between a Mobile Application and a Server is a must.

HTTP Client Server

Communication between a Mobile Application and a Server is usually achieved through the HTTP protocol, where the client (= mobile app) sends a HTTP request to a Server over the Internet. Once the Server has processed the request, it returns the answer back to the client and closes the connection.

Http_request

This is a one-way direction communication, where the communication must always be initiated by the client and consists in a single exchange (send -> receive -> close). The server has no possibility to send anything to the client without having been asked, by the client, to do so.

Most of the time, this means of communication is enough and even recommended. However, if you need to poll the server very frequently, move high-volume of data, react based on events that could occur at the server-side, this way of communication might become a bottleneck.

WebSockets

Some other types of applications such as chat, real-time games, auctions… may require to:

  • have a communication channel that remains open between the client and the server for a longer period than a single request/response scheme
  • have a bi-directional data transmission, where the server could send data to the client without having been requested/polled by the client
  • support data streaming

WebSockets allow the client-side to open and persist a connection to the server.

websockets

A Web socket is a TCP socket connection between the client and the server, over the network, which allows full duplex communication, in other words: data can be transmitted in both directions and at the same time. A TCP socket is an endpoint instance, defined by an IP address and a port.

For a fully detailed documentation on the technical side of the WebSockets, please refer to RFC6455.

WebSockets in Flutter

The web_socket_channel package provides wrappers for WebSocket connections.

To install this package, add the following line to your pubspec.yaml file:

dependencies:
  web_socket_channel: "^1.0.8"

To import the package, add the following 2 imports to your .dart files:

import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/status.dart' as status;

How to connect to the Server?

To connect to the Server, you at least need to know:

  • its TCP socket address (e.g. 192.168.1.25:1234)
  • the name of its web sockets handler (e.g. “/svc/websockets”)
  • the URL scheme (‘ws://’ for plain-text communication or ‘wss://’ for an encrypted channel)
IOWebSocketChannel channel = new IOWebSocketChannel.connect("ws://192.168.1.25:1234/svc/websockets");

Under the hood

This simple line sends a regular HTTP request to the Server, which also contains an “Upgrade” header to inform the Server that the client wishes to establish a WebSocket connection. This request initiates the “handshake” process. If the Server supports the WebSocket protocol, it agrees to upgrade and communicates this through an “Upgrade” header in the response. Once the handshake is complete the initial HTTP connection is replaced by a WebSocket connection that uses the same underlying TCP/IP connection.

The Channel is now open and ready to be used.

Is it safe?

Well, if you only rely on the base protocol “ws://“, it is as safe as the normal “http://” protocol.

Therefore, it is strongly advised to use the same encryption as for HTTPS (TLS/SSL).

To establish such secured communication, you need:

  • a SSL certificate installed on your server
  • use “wss://” rather than ‘ws://
  • point to the SSL port

Extend the security by authenticating the client

As described in a previous article, you could also use the notion of “tokens”, and only allow authenticated clients to connect.

Therefore, you could also pass some extra data to the header during the connection.

The following example illustrates how to pass extra data in the request header.

  ///
  /// Open a new secured WebSocket communication
  ///
  IOWebSocketChannel channel;
	
    try {
      channel = new IOWebSocketChannel.connect(
                  "wss://192.168.1.25:443/svc/websockets",
                  headers: {
                    'X-DEVICE-ID': deviceIdentity,
                    'X-TOKEN': token,
                    'X-APP-ID': applicationId,
                  });
	  ...
    } catch(e){
      ///
      /// An error occurred
      ///
    }

How to close the communication?

The communication channel may be closed by the client, using the following command:

channel.sink.close();

How to send messages to the server?

channel.sink.add('the data I need to send to the server');

How to handle communication from the server?

To be able to accept incoming messages issued by the Server, you need to subscribe (listen) to events from the Stream.

The signature is the following:

StreamSubscription<T> channel.stream.listen(
	void onData(T event),
	{
		Function onError,
		void onDone(),
		bool cancelOnError
	}
);

where:

  • onData: method which is invoked when some data is received from the Server
  • onError: method to handle any errors
  • onDone: method to implement to handle a communication closure (from the Server, for example)
  • cancelOnError: (default false). If set to true, the StreamSubscription is automatically closed at first error event

So, a typical implementation would be:

channel.stream.listen(
	(message){
		// handling of the incoming messages
	},
	onError: function(error, StackTrace stackTrace){
		// error handling
	},
	onDone: function(){
		// communication has been closed 
	}
);

Let’s put all this in practice

Unlike most of the samples you may find on Internet, I am not rewriting the usual Chat application to explain this topic. Instead we are going to build the skeleton of a real-time multiplayers game. Something like a Tic-Tac-Toe game.

The example will consist in:

  • A Websockets server, written in NodeJS
  • A Mobile App Game where:
    • Users will provide their name to join the Game;
    • The list of all players will be refreshed in real-time;
    • One User will select another player to start a new game;
    • Both players will be simulateneously notified an brought to the Tic-Tac-Toe board game;
    • Players will have the possibility to:
      • Resign;
      • Play and their moves will be directly visible on the other player board.

Disclaimer

This sample is only aimed at illustrating the topic. The game skeleton we are going to write is very basic, not complete and subject to huge amount of improvements, validations…

The High-Level view

At first, we need to describe the game from both client and server sides.

Client-Side

The client-side (the Mobile App itself), will consist in 2 screens.

  • Screen 1:

    This screen will allow the user to:

    • Enter a player name and join the game (= action: “join”)
    • See the list of all players who joined the game (= action: “players_list”)
    • Select a player and start a new game (= action: “_newgame”) Both players will then be automatically brought to the second screen.
  • Screen 2:

    This screen will display:

    • The name of the opponent player
    • A resign button. If the user taps this button, the player resigns the game (= action: “resign”). Both players are then brought back to the Screen 1
    • A Tic-Tac-Toe grid, made up of 9 cells.
    • When a player clicks on a cell of the grid, the player symbol (“X” or “O”) will be displayed on both players’ Mobile App

Server-Side

The server-side will only:

  • Record the list of all players and give the players with a unique ID
  • Broadcast that list to all players when a new player joins
  • Record the players of new games
  • Convey one player’s actions to the other game player

Communication Protocol

In order to have communication between the players and server, we need to define some kind of language. We are calling this “protocol”.

All messages that will be sent to the server or from the server to the Mobile App will follow this protocol, which consists in:

  • an action
  • some data

For convenience, I will use the following JSON object (= Map):

{
  "action": "action name",
  "data": "data to be sent"
}

The following diagram shows the protocol: websocket game protocol

The Server-side: WebSocket server

For this very basic WebSocket server, I opted for an implementation in NodeJS, using the “WebSocket” package.

Pre-Requisites

In order to have this work, you need:

  • to have NodeJS (version > 6) installed (refer to NodeJS Website for further details on installing NodeJS).
  • install the “websocket” package use “npm install websocket –save” to install the package

The Source Code

The source code is this WebSocket server is very basic. Let’s first have a look at it, explanation goes along with the code.

/**
* Parameters
*/
var webSocketsServerPort = 34263; // Adapt to the listening port number you want to use
/**
* Global variables
*/
// websocket and http servers
var webSocketServer = require('websocket').server;
var http = require('http');
/**
* HTTP server to implement WebSockets
*/
var server = http.createServer(function(request, response) {
  // Not important for us. We're writing WebSocket server,
  // not HTTP server
});
server.listen(webSocketsServerPort, function() {
  console.log((new Date()) + " Server is listening on port "
      + webSocketsServerPort);
});

/**
* WebSocket server
*/
var wsServer = new webSocketServer({
  // WebSocket server is tied to a HTTP server. WebSocket
  // request is just an enhanced HTTP request. For more info 
  // http://tools.ietf.org/html/rfc6455#page-6
  httpServer: server
});

// This callback function is called every time someone
// tries to connect to the WebSocket server
wsServer.on('request', function(request) {
    
    var connection = request.accept(null, request.origin); 
	
  //
  // New Player has connected.  So let's record its socket
  //
    var player = new Player(request.key, connection);

  //
  // Add the player to the list of all players
  //
    Players.push(player);

  //
  // We need to return the unique id of that player to the player itself
  //
    connection.sendUTF(JSON.stringify({action: 'connect', data: player.id}));

  //
  // Listen to any message sent by that player
  //
    connection.on('message', function(data) {

    //
    // Process the requested action
    //
      var message = JSON.parse(data.utf8Data);
      switch(message.action){
        //
        // When the user sends the "join" action, he provides a name.
        // Let's record it and as the player has a name, let's 
        // broadcast the list of all the players to everyone
        //
          case 'join':
            player.name = message.data;
            BroadcastPlayersList();
            break;

        //
        // When a player resigns, we need to break the relationship
        // between the 2 players and notify the other player 
        // that the first one resigned
        //
          case 'resign':
          console.log('resigned');
            Players[player.opponentIndex]
              .connection
              .sendUTF(JSON.stringify({'action':'resigned'}));

            setTimeout(function(){
              Players[player.opponentIndex].opponentIndex = player.opponentIndex = null;
            }, 0);
            break;

        //
        // A player initiates a new game.
        // Let's create a relationship between the 2 players and
        // notify the other player that a new game starts
        // 
          case 'new_game':
            player.setOpponent(message.data);
            Players[player.opponentIndex]
              .connection
              .sendUTF(JSON.stringify({'action':'new_game', 'data': player.name}));
            break;

        //
        // A player sends a move.  Let's forward the move to the other player
        //
          case 'play':
            Players[player.opponentIndex]
              .connection
              .sendUTF(JSON.stringify({'action':'play', 'data': message.data}));
            break;
      }
    });

  // user disconnected
  connection.on('close', function(connection) {
    // We need to remove the corresponding player
    // TODO
  });
});

// -----------------------------------------------------------
// List of all players
// -----------------------------------------------------------
var Players = [];

function Player(id, connection){
    this.id = id;
    this.connection = connection;
    this.name = "";
    this.opponentIndex = null;
    this.index = Players.length;
}

Player.prototype = {
    getId: function(){
        return {name: this.name, id: this.id};
    },
    setOpponent: function(id){
        var self = this;
        Players.forEach(function(player, index){
            if (player.id == id){
                self.opponentIndex = index;
                Players[index].opponentIndex = self.index;
                return false;
            }
        });
    }
};

// ---------------------------------------------------------
// Routine to broadcast the list of all players to everyone
// ---------------------------------------------------------
function BroadcastPlayersList(){
    var playersList = [];
    Players.forEach(function(player){
        if (player.name !== ''){
            playersList.push(player.getId());
        }
    });

    var message = JSON.stringify({
        'action': 'players_list',
        'data': playersList
    });

    Players.forEach(function(player){
        player.connection.sendUTF(message);
    });
}

The Client-Side: Mobile App

Now that we have the server, let’s consider the Flutter application.

The Websocket Helper

Let’s start by implementing the WebSocket Helper class.

import 'package:flutter/foundation.dart';
import 'package:web_socket_channel/io.dart';

///
/// Application-level global variable to access the WebSockets
///
WebSocketsNotifications sockets = new WebSocketsNotifications();

///
/// Put your WebSockets server IP address and port number
///
const String _SERVER_ADDRESS = "ws://192.168.1.45:34263";

class WebSocketsNotifications {
  static final WebSocketsNotifications _sockets = new WebSocketsNotifications._internal();

  factory WebSocketsNotifications(){
    return _sockets;
  }

  WebSocketsNotifications._internal();

  ///
  /// The WebSocket "open" channel
  ///
  IOWebSocketChannel _channel;

  ///
  /// Is the connection established?
  ///
  bool _isOn = false;
  
  ///
  /// Listeners
  /// List of methods to be called when a new message
  /// comes in.
  ///
  ObserverList<Function> _listeners = new ObserverList<Function>();

  /// ----------------------------------------------------------
  /// Initialization the WebSockets connection with the server
  /// ----------------------------------------------------------
  initCommunication() async {
    ///
    /// Just in case, close any previous communication
    ///
    reset();

    ///
    /// Open a new WebSocket communication
    ///
    try {
      _channel = new IOWebSocketChannel.connect(_SERVER_ADDRESS);

      ///
      /// Start listening to new notifications / messages
      ///
      _channel.stream.listen(
		_onReceptionOfMessageFromServer,
		onError: function(error, StackTrace stackTrace){
			// error handling
		},
		onDone: function(){
			// communication has been closed
			_isOn = false;
		}	
	  );
    } catch(e){
      ///
      /// General error handling
      /// TODO
      ///
    }
  }

  /// ----------------------------------------------------------
  /// Closes the WebSocket communication
  /// ----------------------------------------------------------
  reset(){
    if (_channel != null){
      if (_channel.sink != null){
        _channel.sink.close();
        _isOn = false;
      }
    }
  }

  /// ---------------------------------------------------------
  /// Sends a message to the server
  /// ---------------------------------------------------------
  send(String message){
    if (_channel != null){
      if (_channel.sink != null && _isOn){
        _channel.sink.add(message);
      }
    }
  }

  /// ---------------------------------------------------------
  /// Adds a callback to be invoked in case of incoming
  /// notification
  /// ---------------------------------------------------------
  addListener(Function callback){
    _listeners.add(callback);
  }
  removeListener(Function callback){
    _listeners.remove(callback);
  }

  /// ----------------------------------------------------------
  /// Callback which is invoked each time that we are receiving
  /// a message from the server
  /// ----------------------------------------------------------
  _onReceptionOfMessageFromServer(message){
    _listeners.forEach((Function callback){
      callback(message);
    });
  }
}

This class is a Singleton that handles the communication based on WebSockets.

I implemented it as a Singleton in order to allow its reuse across the whole application without having to care about the connection. The simple fact of importing this .dart file, is enough to use the socket (application-level global variable).

The Game Communication Helper Class

This class is responsible for handling the websockets communication related to the game. This class is also implemented as a Singleton since it will be used by the 2 screens.

import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'websockets.dart';

///
/// Again, application-level global variable
///
GameCommunication game = new GameCommunication();

class GameCommunication {
  static final GameCommunication _game = new GameCommunication._internal();

  ///
  /// At first initialization, the player has not yet provided any name
  ///
  String _playerName = "";

  ///
  /// Before the "join" action, the player has no unique ID
  ///
  String _playerID = "";

  factory GameCommunication(){
    return _game;
  }

  GameCommunication._internal(){
    ///
    /// Let's initialize the WebSockets communication
    ///
    sockets.initCommunication();

    ///
    /// and ask to be notified as soon as a message comes in
    /// 
    sockets.addListener(_onMessageReceived);
  }

  ///
  /// Getter to return the player's name
  ///
  String get playerName => _playerName;

  /// ----------------------------------------------------------
  /// Common handler for all received messages, from the server
  /// ----------------------------------------------------------
  _onMessageReceived(serverMessage){
    ///
    /// As messages are sent as a String
    /// let's deserialize it to get the corresponding
    /// JSON object
    ///
    Map message = json.decode(serverMessage);

    switch(message["action"]){
      ///
      /// When the communication is established, the server
      /// returns the unique identifier of the player.
      /// Let's record it
      ///
      case 'connect':
        _playerID = message["data"];
        break;

      ///
      /// For any other incoming message, we need to
      /// dispatch it to all the listeners
      ///
      default:
        _listeners.forEach((Function callback){
          callback(message);
        });
        break;
    }
  }

  /// ----------------------------------------------------------
  /// Common method to send requests to the server
  /// ----------------------------------------------------------
  send(String action, String data){
    ///
    /// When a player joins, we need to record the name
    /// he provides
    ///
    if (action == 'join'){
      _playerName = data;
    }

    ///
    /// Send the action to the server
    /// To send the message, we need to serialize the JSON 
    ///
    sockets.send(json.encode({
      "action": event,
      "data": data
    }));
  }

  /// ==========================================================
  ///
  /// Listeners to allow the different pages to be notified
  /// when messages come in
  ///
  ObserverList<Function> _listeners = new ObserverList<Function>();

  /// ---------------------------------------------------------
  /// Adds a callback to be invoked in case of incoming
  /// notification
  /// ---------------------------------------------------------
  addListener(Function callback){
    _listeners.add(callback);
  }
  removeListener(Function callback){
    _listeners.remove(callback);
  }
}

Why did I choose to implement a Game Communication Helper on top of the WebSockets Helper?

Simply because, all the logic related to the Game is centralized. Also, because if we would like to extend the game by adding some Chat feature, for example, we would only have to create a specific class which would also rely on the same WebSockets Helper.


Screen 1: Where the user joins and launches a new game

This screen is responsible for:

  • Letting the user join the game, providing a name
  • Maintaining a real-time list of all the players
  • Letting the user start a new game with another player
import 'package:flutter/material.dart';
import 'game_communication.dart';
import 'game_page.dart';

class StartPage extends StatefulWidget {
  @override
  _StartPageState createState() => _StartPageState();
}

class _StartPageState extends State<StartPage> {
  static final TextEditingController _name = new TextEditingController();
  String playerName;
  List<dynamic> playersList = <dynamic>[];

  @override
  void initState() {
    super.initState();
    ///
    /// Ask to be notified when messages related to the game
    /// are sent by the server
    ///
    game.addListener(_onGameDataReceived);
  }

  @override
  void dispose() {
    game.removeListener(_onGameDataReceived);
    super.dispose();
  }

  /// -------------------------------------------------------------------
  /// This routine handles all messages that are sent by the server.
  /// In this page, only the following 2 actions have to be processed
  ///  - players_list
  ///  - new_game
  /// -------------------------------------------------------------------
  _onGameDataReceived(message) {
    switch (message["action"]) {
      ///
      /// Each time a new player joins, we need to
      ///   * record the new list of players
      ///   * rebuild the list of all the players
      ///
      case "players_list":
        playersList = message["data"];
        
        // force rebuild
        setState(() {});
        break;

      ///
      /// When a game is launched by another player,
      /// we accept the new game and automatically redirect
      /// to the game board.
      /// As we are not the new game initiator, we will be
      /// using the "O"
      ///
      case 'new_game':
        Navigator.push(context, new MaterialPageRoute(
          builder: (BuildContext context)
                      => new GamePage(
                            opponentName: message["data"], // Name of the opponent
                            character: 'O',
                        ),
        ));
        break;
    }
  }

  /// -----------------------------------------------------------
  /// If the user has not yet joined, let the user enter
  /// his/her name and join the list of players
  /// -----------------------------------------------------------
  Widget _buildJoin() {
    if (game.playerName != "") {
      return new Container();
    }
    return new Container(
      padding: const EdgeInsets.all(16.0),
      child: new Column(
        children: <Widget>[
          new TextField(
            controller: _name,
            keyboardType: TextInputType.text,
            decoration: new InputDecoration(
              hintText: 'Enter your name',
              contentPadding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
              border: new OutlineInputBorder(
                borderRadius: new BorderRadius.circular(32.0),
              ),
              icon: const Icon(Icons.person),
            ),
          ),
          new Padding(
            padding: const EdgeInsets.all(8.0),
            child: new RaisedButton(
              onPressed: _onGameJoin,
              child: new Text('Join...'),
            ),
          ),
        ],
      ),
    );
  }

  /// ------------------------------------------------------
  /// The user wants to join, so let's send his/her name
  /// As the user has a name, we may now show the other players
  /// ------------------------------------------------------
  _onGameJoin() {
    game.send('join', _name.text);
	
    /// Force a rebuild
    setState(() {});
  }

  /// ------------------------------------------------------
  /// Builds the list of players
  /// ------------------------------------------------------
  Widget _playersList() {
    ///
    /// If the user has not yet joined, do not display
    /// the list of players
    ///
    if (game.playerName == "") {
      return new Container();
    }

    ///
    /// Display the list of players.
    /// For each of them, put a Button that could be used
    /// to launch a new game
    ///
    List<Widget> children = playersList.map((playerInfo) {
        return new ListTile(
          title: new Text(playerInfo["name"]),
          trailing: new RaisedButton(
            onPressed: (){
              _onPlayGame(playerInfo["name"], playerInfo["id"]);
            },
            child: new Text('Play'),
          ),
        );
      }).toList();

    return new Column(
      children: children,
    );
  }

  /// --------------------------------------------------------------
  /// We launch a new Game, we need to:
  ///    * send the action "new_game", together with the ID
  ///      of the opponent we choosed
  ///    * redirect to the game board
  ///      As we are the game initiator, we will play with the "X"
  /// --------------------------------------------------------------
  _onPlayGame(String opponentName, String opponentId){
    // We need to send the opponentId to initiate a new game
    game.send('new_game', opponentId);
	
    Navigator.push(context, new MaterialPageRoute(
      builder: (BuildContext context) 
                  => new GamePage(
                      opponentName: opponentName, 
                      character: 'X',
                    ),
    ));
  }

  @override
  Widget build(BuildContext context) {
    return new SafeArea(
      bottom: false,
      top: false,
      child: Scaffold(
        appBar: new AppBar(
          title: new Text('TicTacToe'),
        ),
        body: SingleChildScrollView(
          child: new Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: <Widget>[
              _buildJoin(),
              new Text('List of players:'),
              _playersList(),
            ],
          ),
        ),
      ),
    );
  }
}

Screen 2: the Game Board

This second screen is reponsible for:

  • displaying the name of the opponent in the AppBar
  • allowing the user to resign, via a ‘Resign’ button
  • displaying the game, real-time, together with all the moves
  • allowing the user to play a move and send the move to the opponent.

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

class GamePage extends StatefulWidget {
  GamePage({
    Key key,
    this.opponentName,
    this.character,
  }): super(key: key);

  ///
  /// Name of the opponent
  ///
  final String opponentName;
  
  ///
  /// Character to be used by the player for his/her moves ("X" or "O")
  ///
  final String character;

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

class _GamePageState extends State<GamePage> {

  ///
  /// One game in terms of grid cells.
  /// When the user plays, one of this cells is filled with "X" or "O"
  ///
  List<String> grid = <String>["","","","","","","","",""];

  @override
  void initState(){
    super.initState();
    ///
    /// Ask to be notified when a message from the server
    /// comes in.
    ///
    game.addListener(_onAction);
  }

  @override
  void dispose(){
    game.removeListener(_onAction);
    super.dispose();
  }

  /// ---------------------------------------------------------
  /// The opponent took an action
  /// Handler of these actions
  /// ---------------------------------------------------------
  _onAction(message){
    switch(message["action"]){
      ///
      /// The opponent resigned, so let's leave this screen
      ///
      case 'resigned':
          Navigator.of(context).pop();
        break;

      ///
      /// The opponent played a move.
      /// So record it and rebuild the board
      ///
      case 'play':
          var data = (message["data"] as String).split(';');
          grid[int.parse(data[0])] = data[1];

          // Force rebuild
          setState((){});
        break;
    }
  }

  /// ---------------------------------------------------------
  /// This player resigns
  /// We need to send this notification to the other player
  /// Then, leave this screen
  /// ---------------------------------------------------------
  _doResign(){
    game.send('resign', '');
    Navigator.of(context).pop();
  }

  @override
  Widget build(BuildContext context) {
    return new SafeArea(
      top: false,
      bottom: false,
      child: new Scaffold(
        appBar: new AppBar(
          title: new Text('Game against: ${widget.opponentName}', style: new TextStyle(fontSize: 16.0)),
          actions: <Widget>[
            new RaisedButton(
              onPressed: _doResign,
              child: new Text('Resign'),
            ),
          ]
        ),
        body: _buildBoard(),
      ),
    );
  }

  /// --------------------------------------------------------
  /// Builds the Game Board.
  /// --------------------------------------------------------
  Widget _buildBoard(){
    return new SafeArea(
      top: false,
      bottom: false,
      child: new GridView.builder(
        gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
        ),
        itemCount: 9,
        itemBuilder: (BuildContext context, int index){
          return _gridItem(index);
        },
      ),
    );
  }

  Widget _gridItem(int index){
    Color color = grid[index] == "X" ? Colors.blue : Colors.red;

    return new InkWell(
      onTap: () {
        ///
        /// The user taps a cell.
        /// If the latter is empty, let's put this player's character
        /// and notify the other player.
        /// Repaint the board
        ///
        if (grid[index] == ""){
          grid[index] = widget.character;

          ///
          /// To send a move, we provide the cell index
          /// and the character of this player
          ///
          game.send('play', '$index;${widget.character}');

          /// Force the board repaint
          setState((){});
        }
      },
      child: new GridTile(
        child: new Card(
          child: new FittedBox(
            fit: BoxFit.contain,
            child: new Text(grid[index], style: new TextStyle(fontSize: 50.0, color: color,))
          ),
        ),
      ),
    );
  }
}


The main

Finally, we have the main routine that simply launches the first screen.

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

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'WebSockets Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new StartPage(),
    );
  }
}


The results

The following video shows 2 mobile devices running this sample application. As you may see, interactions are real-time.

result result

Conclusions

WebSockets are easy to implement and are essential when a Mobile App needs to deal with real-time and full-duplex communication.

I hope that this article demystified the concept of WebSockets through this practical example, which, once again, is only aimed at demonstrating the communication using WebSockets.

Stay tuned for new articles and, happy coding.