Part 3/3 - How to create a server-side timer using WebSockets (with Socket.IO), NestJS and Flutter
Tags: websockets, flutter, nestjs, realtime
Published: 2022-08-15 15:30:00

## Introduction

In this part 3/3, I will show you how to implement WebSocket functionality in a new Flutter app and connect it to the NestJS backend server application that we implemented in part 2/3.

**The code segments in this article are written for reading purpose, the full source code is available on my GitHub, linked at the end.**

## 1. Create a new flutter project

I’m using Flutter 2.5.2 with null safety. A later version should support as well, but didn’t test with a newer version. 

You can create a new Flutter project by executing following command.

```
flutter create server_timer_frontend
```

This can take a couple of minutes.

## 2. Install Socket.IO client package for Flutter

After your Flutter project is created, we can add the package of the socket client we are going to use. 

I have tested several packages for this purpose. It was very difficulty to find a proper one because for our WebSocket connection to work successfully, both the backend Socket.IO version and the frontend client Socket.IO version **must** be compatible. So I had to do some trial and error and some online research to find out the best package.

The best package I found is named **socket_io_client** and its latest update (version 2.0.0) was released a few weeks ago (July of 2022). You can see more info at https://pub.dev/packages/socket_io_client.  

You can install it by running

```
flutter pub add socket_io_client
```

One side note - the official Flutter documentation shows a way to implement WebSocket connection using a package called **web_socket_channel**, it was not working for me with my NestJS backend.

Before writing code, create a new folder called **sockets** within the **lib** folder, where we will put all of our code related to sockets. You can refactor them as however you like later.

## 3. Create socket service (socket_service.dart)

In the frontend, we have to have several files related to this implementation. One important file is **socket_service.dart**. Here we define methods such as `connectAndListenToSocket(){}`, `disconnectSocket(){}`, `disposeSocket(){}` which are important for our WebSocket communication.

The following is the abstract structure of the `SocketService` class.

```dart
class SocketService {
  static Socket? socket;

  // connectAndListenToSocket(){}

  // disconnectSocket(){}

  // disposeSocket(){}
}
```

First, write a method named `connectAndListenToSocket(String authToken, String deviceId){}`.

Next, within the `connectAndListenToSocket(){}` method, we have to set up the connection to the backend. Since the backend requires a user auth token, we have to send that as well with this. 

**Please edit the IP address according to your network connection. Otherwise this will not connect to the NestJS backend. You can view your IP from network settings of your computer.**

The default port of the NestJS app is 3000. That is written in right side of the semicolon (`:`) as in the following code. (Replace the 192.168.X.X with your actual IP address)

```dart
    socket = io(
        'http://192.168.X.X:3000', // Replace with your network IP 
        OptionBuilder()
            .setTransports(['websocket'])
            .setAuth({
              'token': 'Bearer $authToken',
            })
            .setQuery({
              'deviceId': deviceId,
            })
            .disableAutoConnect()
            .build());
```

It takes two parameters, the user `authToken` (you can get this from Firebase Auth or any other auth method), the `deviceId` (you can get this by using **device_info_plus** package (https://pub.dev/packages/device_info_plus).

We can use the `setQuery()` method to send query parameters in the body of our message. This body can be caught by using the `@MessageBody()` decorator in NestJS.

The `connectAndListenToSocket(){}` method will try to connect the socket to backend. 

Next, write the following piece of code which tries to actually start the WebSocket connection if it is not yet started.

```dart
    if (!socket!.connected) {
      socket!.connect();
      print('connecting....');
    }
```

Next, inside this `connectAndListenToSocket(){}` method, we can set up some listeners which are for main events such as `onConnect(){}`, `onDisconnect(){}`, `onError(){}` and `onConnectError(){}` etc. as below.

```dart
    socket!.onConnect((_) {
      print('connected and listening to socket!.');
    });

    socket!.onDisconnect((_) => print('disconnected from socket!.'));

    socket!.onError((data) => print(data));

    socket!.onConnectError((data) => {print(data)});
```

We can listen in the frontend, to the custom events set by us in the backend. Therefore, whenever the backend server sends a message under that event, this listener would be fired.

Next, write the following listener which listens to the `tick` event of our server-side timer app. We defined this event in the backend NestJS application.

```dart
    // When the message event 'tick' received from server, that data is added to a stream 'streamSocket'.
    socket!.on(TimerEvents.tick.toString().split('.').last, (data) {
      streamSocket.addResponse(data['timer'].toString());
    });
```

In that listener, we add the received data to a Flutter `StreamController` using the `streamSocket.addResponse(data["timer"].toString());` method call. The variable `data` is the payload that we send from the backend. The value of the key `timer` contains the actual time in seconds.

---

Finally write the two functions for disconnecting the socket and disposing the socket.

```dart
  static disconnectSocket() async {
    socket!.disconnect();
  }

  static disposeSocket() async {
    socket!.dispose();
  }
```

The difference between the two is, `disconnectSocket(){}` does **not** remove the listeners created in our Flutter app for the WebSocket connection, but the `disposeSocket(){}` does.

If we connect to the same socket again, the multiple listeners would be present if we had only disconnected, and not disposed the WebSocket connection. Therefore, to start everything fresh next time, it's a good idea to `disposeSocket(){}` the socket when need to finish the WebSocket connection.

## 4. Create timer service (timer_service.dart)

In this service file, I will define the methods `startServerTimer` and `stopServerTimer`.

```dart
class TimerService {

  // startServerTimer(){}
  
  // stopServerTimer(){}

}
```

Whenever we want to send something to the backend, we use the `socket.emit()` method call. This method call would accept a first string parameter - the name of the event, and then the second parameter is the data to be sent as a JSON object. 

We can accept this JSON object in our backend easily - using the `@MessageBody()` decorator in NestJS.

```dart
  // Timer methods.
  static startServerTimer(int duration) {
    if (SocketService.socket!.connected) {
      SocketService.socket!.emit(TimerEvents.timerStart.toString().split('.').last, {
        "dur": duration,
      });
    } else {
      print("No socket connection found.");
    }
  }

  static stopServerTimer() {
    if (SocketService.socket!.connected) {
      SocketService.socket!.emit(TimerEvents.timerStop.toString().split('.').last);
    } else {
      print("No socket connection found.");
    }
  }
```

## 5. Stream socket (stream_socket.dart)

This file is not essential for the socket implementation but I’m using it to store data received from the WebSocket connection and use it to update the UI easily using a `StreamBuilder` Flutter widget in my countdown timer example.

```dart
class StreamSocket {
  final StreamController _socketResponse = StreamController<String>.broadcast();
  Function(String) get addResponse => _socketResponse.sink.add;
  Stream<String> get getResponse => _socketResponse.stream.asBroadcastStream() as Stream<String>;
}

StreamSocket streamSocket = StreamSocket();
```

Of course there can be many other alternative ways to do this part. I would love to hear them in the comments below.

Don’t forget to import this file inside the **socket_service.dart** file.

## 6. Events file

In the flutter side also, I’m maintaining a **socket_events.dart** file to keep the needed socket events as enums, just like we did in NestJS in the part 2/3 of this tutorial series.

```dart
enum TimerEvents {
  tick,
  timerStart,
  timerStop,
}
```

Don’t forget to import this file inside the **socket_service.dart** file.

## 7. Add the UI to display socket data (main.dart)

The abstract structure of the **main.dart** is as follows.

```dart
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  
  // Setup MaterialApp

}

class MyHomePage extends StatefulWidget {

  // Initiate the state.

}

class _MyHomePageState extends State<MyHomePage> {

  // _convertToDisplayTime(){}

  // initState(){}

  // build(){}

}
```

First, clear out the comments from the default Flutter application.

Next, the important thing is, we have to write code inside the `_MyHomePageState(){}` class. Since the code is a bit long, I'll mention only the important things here, because you can get the full source code from the GitHub link at the end of the article.

We declare following function to convert the data from our backend to proper timer format for display.

```dart
  // This is to convert the time in seconds to a string with the format '00:00'.
  String _convertToDisplayTime(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, "0");
    String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
    String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
    if (duration.inHours == 0) {
      return "$twoDigitMinutes:$twoDigitSeconds";
    }
    return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds";
  }
```

When the WebSocket connection is established and connected, we are rendering a `StreamBuilder` widget on screen to display the data from that connection using the **stream_socket.dart** file we created earlier.

```dart
            (SocketService.socket != null && SocketService.socket!.connected)
                ? StreamBuilder(
                    stream: streamSocket.getResponse,
                    builder: (context, snapshot) {
                      if (snapshot.hasData) {
                        if (snapshot.data.toString() == "0") {
                          return const Text("Socket Status : TICKING TIMEOUT");
                        } else {
                          return Text(
                            'Socket Status : TIMER TICKING - ${_convertToDisplayTime(
                              Duration(
                                seconds: int.parse(snapshot.data.toString()),
                              ),
                            ).toString()}',
                          );
                        }
                      } else {
                        if (SocketService.socket!.connected) {
                          return const Text("Socket Status : CONNECTED");
                        } else {
                          return const Text("Socket Status : DISCONNECTED");
                        }
                      }
                    },
                  )
                : const Text('Socket Status : DISCONNECTED'),
```

Then we have to create the buttons which are there for the functions such as connect socket, disconnect, dispose, start timer and stop timer.

---

Here is the **Connect** button code.

```dart
            ElevatedButton.icon(
              onPressed: () {
                // Populate these two values with your own values.
                // Get token from Firebase Auth or other authentication service.
                // Get deviceId using `device_info_plus` package.
                SocketService.connectAndListenToSocket('token', 'deviceId'); // Start a socket channel with the server.

                // Update the UI when anything change in the socket.
                SocketService.socket!.onAny((event, data) {
                  setState(() {});
                });
              },
              label: const Text('Connect Socket'),
              icon: const Icon(Icons.connect_without_contact_rounded),
              style: ElevatedButton.styleFrom(
                primary: Colors.purple,
              ),
            ),
```

---

Here are the **Start timer** and **Stop timer** buttons code.

```dart
            ElevatedButton.icon(
              onPressed: () {
                TimerService.startServerTimer(10); // Start the server-side timer with 10 seconds.
              },
              label: const Text('Start Server Countdown'),
              icon: const Icon(Icons.timer),
              style: ElevatedButton.styleFrom(
                primary: Colors.green[900],
              ),
            ),
            ElevatedButton.icon(
              onPressed: () {
                TimerService.stopServerTimer();
              },
              label: const Text('Stop Server Countdown'),
              icon: const Icon(Icons.timer_off),
              style: ElevatedButton.styleFrom(
                primary: Colors.blue[900],
              ),
            ),
```

Here we are just calling the methods defined in `TimerService` to start a timer on the NestJS server or to stop it.

---

The disconnect socket does not remove the listeners, but dispose does. You can see the code for the relevant **Disconnect socket** and **Dispose socket** buttons below.

```dart
            ElevatedButton.icon(
              onPressed: () {
                // Disconnect from the socket. (Does not remove the listeners.)
                SocketService.disconnectSocket();
              },
              label: const Text('Disconnect Socket\n(Does not remove the listeners)'),
              icon: const Icon(Icons.remove_done),
              style: ElevatedButton.styleFrom(
                primary: Colors.purple,
              ),
            ),
            ElevatedButton.icon(
              onPressed: () {
                // Disconnect the socket and remove the listeners.
                SocketService.disposeSocket();
              },
              label: const Text('Dispose Socket\n(Fresh Start - removes all listeners)'),
              icon: const Icon(Icons.cancel),
              style: ElevatedButton.styleFrom(
                primary: Colors.purple,
              ),
            ),
```

That is all about the frontend UI part of the code!

## Common errors

- Error 1
```
WebSocketException: Connection to 'http://192.168.X.X:3000/socket.io/?deviceId=deviceId&EIO=4&transport=websocket#' was not upgraded to websocket
```
Make sure you have correctly set the `SocketsGateway` as a **provider** in **app.module.ts** in the NestJS backend.

- Error 2

```
SocketException: OS Error: Network is unreachable, errno = 101, address = 192.168.X.X, port = XXXXX
```
Make sure you are connected to the correct network connection and that you are using the correct IP.

## Conclusion

That’s it. We have completed the flutter frontend implementation of WebSocket communication.

Hooray! 🥳🎉🎉🎉🥳🥳🥳🎉🥳🎉🎉 You've reached this far! Give yourself a pat on the back - you deserve it. 

I hope now you have some idea on how to implement a server-side timer using WebSockets(with Socket.IO), NestJS and Flutter. There may have been alternative ways to do some of the things that I have done in this tutorial series and I would love to hear about them in the comments section below.

Also, don't hesitate to point out any mistakes that I have made because I'm also learning this stuff! 

Please share your solution as well if you follow this series and make your own - I would love to see it. Have a wonderful day - Peace out ✌️

## Support me!

Do you think I deserve a cup of coffee for this article? 😃

<a href="https://www.buymeacoffee.com/rukshanjs" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

## Video

[![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/kx7LQNNmTX4/0.jpg)](https://www.youtube.com/watch?v=kx7LQNNmTX4)

## Source code

You can find the full source code of the project at my GitHub, https://github.com/RukshanJS/websockets-nestjs-flutter

## References

1. socket_io_client connection issue - https://stackoverflow.com/q/71679446