# Build a chat application in Dart (Part 3)

In [Part 2](https://creativebracket.com/build-a-chat-application-in-dart-part-2/) we refactored our solution by splitting the Sign in and Chat room logic into `View` classes, building a `Router` object to transition between the screens. We will complete the full flow here by implementing our WebSocket connection and managing each of the chatters in our Chat room. At the end of this tutorial we will have a functioning chat application.

[Get the source code](https://github.com/graphicbeacon/dart-bulma-chat-app/tree/part-3)

## **1\. Create a WebSocket object**

In order for messages to be relayed from one user and broadcasted to the others, we need to establish a persistent, *bi-directional* connection to our server. This will allow messages to be sent back and forth between the server and client using an event-based architecture. This is made possible via the WebSocket protocol, which the **dart:io** and **dart:html** library provides classes for. *So no external packages needed!*

Let’s begin on the client by writing a class to be responsible for instantiating a `WebSocket` object and implementing various event listeners on them. This class will represent a user’s connection to our Chat room server.

### **Build a Chat room subject**

Create a `chat_room_subject.dart` file inside the directory structure `lib/src` and implement the class for our `ChatRoomSubject`:

```dart
// lib/src/chat_room_subject.dart

import 'dart:html';

class ChatRoomSubject {
  ChatRoomSubject(String username) {}
  
  final WebSocket socket;
}
```

Because we’ve marked our `socket` instance variable as **final**, it requires that we assign a value to it in the initialize phase, that is, before the `{}` part of our constructor is run. Let’s therefore create our `WebSocket` instance and pass the URL to our server:

```dart
ChatRoomSubject(String username)
  : socket = WebSocket('ws://localhost:9780/ws?username=$username') {}
```

When our `WebSocket` class is instantiated, a connection will be established to the URL passed in as it’s `String` argument. We’ve also passed in the `username` value as part of the query string of the WebSocket URL.

We are now able to listen for several events on our `WebSocket` namely **open**, **error** and **close**.

```dart
// ..
class ChatRoomSubject {
  // ..
  // ..
  _initListeners() {
    socket.onOpen.listen((evt) {
      print('Socket is open');
    });

    // Please note: "message" event will be implemented elsewhere

    socket.onError.listen((evt) {
      print('Problems with socket. ${evt}');
    });

    socket.onClose.listen((evt) {
      print('Socket is closed');
    });
  }
}
```

And we need to invoke the `_initListeners()` method:

```dart
ChatRoomSubject(String username)
  : socket = WebSocket('ws://localhost:9780/ws?username=$username') {
  _initListeners(); // <-- Added this line
}
```

We need to be able to send messages to our Chat room server and close a WebSocket connection, effectively leaving the Chat room. Add these methods to our `ChatRoomSubject` class before `_initListeners()`:

```dart
send(String data) => socket.send(data);
close() => socket.close();
```

### **Integrate** `ChatRoomSubject` into the `ChatRoomView` class

Import the dart file we’ve just created from `web/views/chat_room.dart` directly after our `// Absolute imports` (`dart_bulma_chat_app` *is a reference to the* `name` *value field inside your* ***pubspec.yaml*** *file, which points to the* `lib/` *directory*):

```dart
// web/views/chat_room.dart

// Absolute imports
import 'dart:html';
import 'dart:convert';

// Package imports
import 'package:dart_bulma_chat_app/src/chat_room_subject.dart'; // <-- Added this line
```

And then we will instantiate this class as follows:

```dart
// ..
// ..
class ChatRoomView implements View {
  ChatRoomView(this.params)
      : _contents = DocumentFragment(),
        _subject = ChatRoomSubject(params['username']) // <-- Added this line
  {
    onEnter();
  }

  /// Properties
  final ChatRoomSubject _subject;
  // ..
  // ..
}
```

If you look at the `_initListeners()` method in `ChatRoomSubject` you should notice that we didn’t implement the listener for the WebSocket “**message**” event. We will do that in `ChatRoomView` since we have access to the `WebSocket` instance:

```dart
// ..
// ..
class ChatRoomView implements View {
  // ..
  // ..
  void _addEventListeners() {
    // ..
    // ..
    _subject.socket.onMessage.listen(_subjectMessageHandler);
  }
  
  void _subjectMessageHandler(evt) {
    chatRoomLog.appendHtml(evt.data + '<br />');
  }
}
```

Lastly, let’s amend `_sendBtnClickHandler(e)` to send messages from the input field to the Chat server, empty the message input field value and reset it’s focus:

```dart
void _sendBtnClickHandler(e) {
  _subject.send(messageField.value);

  // Resets value and re-focuses on input field
  messageField
    ..value = ''
    ..focus();
}
```

## **2\. Upgrade incoming requests to WebSocket connections**

To get a basic example working, let’s write some server-side logic to maintain our chat room session.

Create `lib/src/chat_room_session.dart` and implement the class for our `ChatRoomSession`:

```dart
import 'dart:io';
import 'dart:convert';

class Chatter {
  Chatter({this.session, this.socket, this.name});
  HttpSession session;
  WebSocket socket;
  String name;
}

class ChatRoomSession {
  final List<Chatter> _chatters = [];

  // TODO: Implement addChatter method
  // TODO: Implement removeChatter method
  // TODO: Implement notifyChatters method
}
```

Here we have two different classes. The `Chatter` class represents each participant. This participant contains its current **session**, Web**Socket** connection and **name**. The `ChatRoomSession` class manages our list of participants, including facilitating communication between them.

Here’s the implementation of the `addChatter()` method. This upgrades the incoming request to a WebSocket connection, builds a `Chatter` object, creates event listeners and adds it to the `_chatters` list:

```dart
class ChatRoomSession {
  final List<Chatter> _chatters = [];

  addChatter(HttpRequest request, String username) async {
    WebSocket ws = await WebSocketTransformer.upgrade(request);
    Chatter chatter = Chatter(
      session: request.session,
      socket: ws,
      name: username,
    );

    // Listen for incoming messages, handle errors and close events
    chatter.socket.listen(
      (data) => _handleMessage(chatter, data),
      onError: (err) => print('Error with socket ${err.message}'),
      onDone: () => _removeChatter(chatter),
    );

    _chatters.add(chatter);

    print('[ADDED CHATTER]: ${chatter.name}');
  }
  
  // TODO: Implement _handleMessage method
  _handleMessage(Chatter chatter, String data) {}
}
```

When messages are sent to the server via the connection, we handle this by invoking `_handleMessage()` while passing it the `Chatter` object and `data` as arguments.

Let’s cross out that `// TODO:` by implementing `_handleMessage()`:

```dart
_handleMessage(Chatter chatter, String data) {
  chatter.socket.add('You said: $data');
  _notifyChatters(chatter, data);
}
```

And let’s implement `_notifyChatters()`:

```dart
_notifyChatters(Chatter exclude, [String message]) {
  _chatters
    .where((chatter) => chatter.name != exclude.name)
    .toList()
    .forEach((chatter) => chatter.socket.add(message));
}
```

And also removing the `Chatter` object when a participant leaves:

```dart
_removeChatter(Chatter chatter) {
  print('[REMOVING CHATTER]: ${chatter.name}');
  _chatters.removeWhere((c) => c.name == chatter.name);
  _notifyChatters(chatter, '${chatter.name} has left the chat.');
}
```

### **Instantiate our** `ChatRoomSession` class

Go to `bin/server.dart` and create our instance:

```dart
import 'dart:io';
import 'dart:convert';

import 'package:dart_bulma_chat_app/src/chat_room_session.dart'; // <-- Added this line

main() async {
  // TODO: Get port from environment variable
  var port = 9780;
  var server = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
  var chatRoomSession = ChatRoomSession(); // <-- Added this line
  // ..
  // ..
}
```

Scroll down to the `case '/ws':` section and call the `addChatter()` method, passing it the **request** and **username**:

```dart
// ..
// .. 
  case '/ws':
    String username = request.uri.queryParameters['username'];
    chatRoomSession.addChatter(request, username);
    break;
// ..
// ..
```

So we retrieve the **username** from the query parameters when we passed `?username=$username` into the URL we defined for our `WebSocket` instance inside `ChatRoomSubject`.

Let us test what we have now. Open a terminal session and instantiate the server:

```bash
$ dart bin/server.dart
```

And run the web app on another terminal session:

```bash
$ webdev serve --live-reload
```

Following this fully should give you the image result below:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1725926036310/2c083e6e-f121-4b86-8d3c-834c54172de9.gif align="center")

If you’ve got this far then great job!!!

## **3\. Implement some helper functions**

We need a way of categorising the types of messages sent to our Chat server and respond to it accordingly. When a participant joins the chat, it would be great to send a notification welcoming this new participant and notifying the other participants on the new chatter. We will create an **enum** type which will contain the particular types of messages this Chat app will have.

Create `lib/src/helpers.dart` and let’s define our **enum**:

```dart
enum ActionTypes { newChat, chatMessage, leaveChat }
```

This will affect the format of our messages so that it gives us the flexibility of controlling how these messages are presented. We will now be sending a `Map` structure encoded as JSON Strings, looking like this:

```dart
{
  "type": <ActionType>,
  "from": <username sending this message>,
  "data": <the message>,
}
```

If you’ve ever used State Management solutions like Redux, we are essentially building our “action” payload which will be handled later on by some “reducer” logic.

We need to add these functions which will build our `Map` structure including encoding and decoding them:

```dart
// lib/src/helpers.dart
import 'dart:convert'; // <-- Added this import

enum ActionTypes { newChat, chatMessage, leaveChat }

// Added the functions below
encodeMessage(ActionTypes type, String from, String message) => json.encode({
      'type': type.toString(),
      'from': from,
      'data': message,
    });

decodeMessage(String message) {
  var decoded = json.decode(message);
  return {
    'type': getActionType(decoded['type']),
    'from': decoded['from'],
    'data': decoded['data'],
  };
}
```

In the `encodeMessage` function, we are sending the **enum** value as a string by invoking its `toString()` method. In `decodeMessage` we are passing this value into another function which will return the matching **enum**. This helps us to ensure the enums are the “source of truth” for managing our message types.

We now need to implement `getActionType`:

```dart
ActionTypes getActionType(String typeStr) {
  ActionTypes matchedActionType;
  ActionTypes.values.forEach((actionType) {
    if (actionType.toString() == typeStr) {
      matchedActionType = actionType;
    }
  });
  return matchedActionType;
}
```

`getActionType` will take the `ActionType` **enum** that was encoded into a String earlier and decode it into an actual `ActionType` object for use in our logic.

### **Refactor our** `ChatRoomSession` class

Now let’s get back to `lib/src/chat_room_session.dart` and use our helper functions:

```dart
// Import our helpers first
import 'package:dart_bulma_chat_app/src/helpers.dart';

// ..
// ..

// Update _handleMessage implementation
_handleMessage(Chatter chatter, data) {
  print('[INCOMING MESSAGE]: $data');

  // Decode and check action types of payload
  Map<String, dynamic> decoded = json.decode(data);
  var actionType = getActionType(decoded['type']);
  var message = decoded['data'];

  // Reducer logic to handle different chat action types
  switch (actionType) {
    case ActionTypes.newChat:
      // TODO: Welcome connected participant
      // TODO: Tell the others of this new participant
      break;
    case ActionTypes.chatMessage:
      // TODO: Display the message on the participant's screen using 'You' instead of their name
      // TODO: Broadcast the chat message to the other participants using the participant's username
      break;
    case ActionTypes.leaveChat:
      // TODO: Cancel the socket when a participant leaves. 
      // TODO: This will trigger the onDone event we defined earlier, removing its associated `Chatter` object from _chatters
      break;
    default:
      break;
  }
}
```

And now to implement the `// TODO:` blocks for `_handleMessage()`:

```dart
case ActionTypes.newChat:
  // Welcome connected participant
  chatter.socket.add(encodeMessage(
    ActionTypes.newChat,
    null,
    'Welcome to the chat ${chatter.name}',
  ));

  // Tell the others of this new participant
  _notifyChatters(
    ActionTypes.newChat,
    chatter,
    '${chatter.name} has joined the chat.',
  );
  break;

case ActionTypes.chatMessage:
  // Display the message on the participant's screen using 'You' instead of their name
  chatter.socket.add(encodeMessage(
    ActionTypes.chatMessage,
    'You',
    message,
  ));

  // Broadcast the chat message to the other participants using the participant's username
  _notifyChatters(ActionTypes.chatMessage, chatter, message);
  break;

case ActionTypes.leaveChat:
  // Cancel the socket when a participant leaves. 
  // This will trigger the onDone event we defined earlier, removing its associated `Chatter` object from _chatters
  chatter.socket.close();
  break;
```

And here’s how we update `_notifyChatters()`:

```dart
_notifyChatters(ActionTypes actionType, Chatter exclude, [String message]) {
  // Set the from value dependent on the action type. When set to null, its treated as a generic message from the chat server
  var from = actionType == ActionTypes.newChat || actionType == ActionTypes.leaveChat ? null : exclude.name;

  _chatters
    .where((chatter) => chatter.name != exclude.name)
    .toList()
    .forEach((chatter) => chatter.socket.add(encodeMessage(
      actionType,
      from,
      message,
    )));
}
```

And now update `_removeChatter()`:

```dart
_removeChatter(Chatter chatter) {
  print('[REMOVING CHATTER]: ${chatter.name}');
  _chatters.removeWhere((c) => c.name == chatter.name);
  _notifyChatters(
    ActionTypes.leaveChat,
    chatter,
    '${chatter.name} has left the chat.',
  );
}
```

### **Refactor the** `ChatRoomView` class

To get this working, we need to build the correct JSON string for our `ChatRoomSession` object to correctly decode and handle.

Open `web/views/chat_room.dart` and import our `lib/src/helpers.dart` file:

```dart
// Package imports
import 'package:dart_bulma_chat_app/src/chat_room_subject.dart';
import 'package:dart_bulma_chat_app/src/helpers.dart'; // <-- Added this line
```

And then amend `_sendBtnClickhandler(e)`:

```dart
void _sendBtnClickHandler(e) {
  _subject.send(encodeMessage(
    ActionTypes.chatMessage,
    params['username'],
    messageField.value,
  ));

  // Resets value and re-focuses on input field
  messageField
    ..value = ''
    ..focus();
}
```

Stop and start the `bin/server.dart` file and try again:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1725926271162/aa2d0362-4a90-4ab9-8244-ae784c7b9d6f.gif align="center")

## **4\. Prettify chat log and messages**

Currently when a user joins the chat, the other participants are not alerted. Add this line under `_initListeners()` in `lib/src/chat_room_subject.dart`:

```dart
import 'package:dart_bulma_chat_app/src/helpers.dart'; // <-- Added this import at the top

// ..
// ..

_initListeners() {
  socket.onOpen.listen((evt) {
    print('Socket is open');
    send(encodeMessage(ActionTypes.newChat, null, null)); // <-- Added this line
  });
  // ..
  // ..
}
```

Testing this will output the below to the other participants:

```json
{ 
  "type": "ActionTypes.newChat", 
  "from": null, 
  "data": null 
}
```

That object will be handled by the `case ActionTypes.newChat:` section in `lib/src/chat_room_session.dart`.

Now we will prettify the chat messages by amending the `_subjectMessageHandler(evt)` method in `web/views/chat_room.dart` with some helper class names from the Bulma CSS library:

```dart
void _subjectMessageHandler(evt) {
  var decoded = decodeMessage(evt.data);
  var from = decoded['from'];
  var message = decoded['data'];
  var result;

  if (from == null) {
    result = '''
    <div class="tags">
      <p class="tag is-light is-normal">$message</p>
    </div>
    ''';
  } else {
    result = '''
      <div class="tags has-addons">
        <span class="tag ${from == 'You' ? 'is-primary' : 'is-dark'}">$from said:</span>
        <span class="tag is-light">$message</span>
      </div>
    ''';
  }

  chatRoomLog.appendHtml(result);
}
```

And here we are!

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1725926388005/d2c7df0b-7c88-4050-9ef7-a27be186c014.gif align="center")

## **5\. Implement log out functionality**

At the moment you are automatically logged out when you refresh the page. Let’s have the user click a button to log out instead.

In `web/views/chat_room.dart` amend the `_contents.innerHtml` value in the `prepare()` method by replacing `<h1 class="title">Chatroom</h1>` with this:

```xml
<div class="columns">
  <div class="column is-two-thirds-mobile is-two-thirds-desktop">
    <h1 class="title">Chatroom</h1>
  </div>
  <div class="column has-text-right">
    <button
      id="ChatRoomLeaveBtn"
      class="button is-warning">Leave Chat</button>
  </div>
</div>
```

And make these further amendments:

```dart
/// Properties
// ..
ButtonElement leaveBtn; // <-- Added this line to list of instance props

// ..
// ..

@override
void onExit() {
  _removeEventListeners();
  _subject.close(); // <-- Added this line. Ends the chat session

  router.go('/'); // And then redirects to Sign in screen
}

@override
void onPrepare() {
  // ..
  // ..
  sendBtn = chatRoomBox.querySelector('#ChatRoomSendBtn');
  leaveBtn = chatRoomBox.querySelector('#ChatRoomLeaveBtn'); // <-- Added this line

  _addEventListeners();
}

_addEventListeners() {
  // ..
  // ..
  leaveBtn.addEventListener('click', _leaveBtnClickHandler); // <-- Added this line
}

_removeEventListeners() {
  // ..
  // ..
  leaveBtn.removeEventListener('click', _leaveBtnClickHandler);
}

// Add this method
void _leaveBtnClickHandler(e) => onExit();
```

Awesome! We’ve got it.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1725926597509/48ab5e64-3d16-4ef3-b990-db663d17edac.gif align="center")

[Get the source code](https://github.com/graphicbeacon/dart-bulma-chat-app/tree/part-3)

## **Conclusion**

It’s amazing to see what we achieved using the Dart SDK with it’s suite of libraries. Although we have the full flow, this is still running in development mode which is why we are running two servers. Ideally we should be able to spin up a single server to manage the static serving of the `index.html` file as well as our Chat server.

In Part 4, we will look at bundling our app and deploying to Heroku. This would involve amending the `default:` block of the switch statement in `bin/server.dart` to forward other requests to a static directory. Feel free to look into this if you wish. Also, leave below any comments or feedback you have. I do read them :)

## **Further reading**

1. [WebSocket class documentation](https://api.dartlang.org/stable/2.2.0/dart-io/WebSocket-class.html)
    
2. [Capture and Handle Data Sequences with Streams in Dart (Free video lesson)](https://egghead.io/lessons/flutter-capture-and-handle-data-sequences-with-streams-in-dart)
    
3. [Bulma CSS Library](https://bulma.io/)
