DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Part 3/3 - How to create a server-side timer using WebSockets (with Socket.IO), NestJS and Flutter
Rukshan J. Senanayaka
Rukshan J. Senanayaka

Posted on

Part 3/3 - How to create a server-side timer using WebSockets (with Socket.IO), NestJS and Flutter

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

class SocketService {
  static Socket? socket;

  // connectAndListenToSocket(){}

  // disconnectSocket(){}

  // disposeSocket(){}
}
Enter fullscreen mode Exit fullscreen mode

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)

    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());
Enter fullscreen mode Exit fullscreen mode

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.

    if (!socket!.connected) {
      socket!.connect();
      print('connecting....');
    }
Enter fullscreen mode Exit fullscreen mode

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.

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

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

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

    socket!.onConnectError((data) => {print(data)});
Enter fullscreen mode Exit fullscreen mode

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.

    // 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());
    });
Enter fullscreen mode Exit fullscreen mode

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.

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

  static disposeSocket() async {
    socket!.dispose();
  }
Enter fullscreen mode Exit fullscreen mode

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.

class TimerService {

  // startServerTimer(){}

  // stopServerTimer(){}

}
Enter fullscreen mode Exit fullscreen mode

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.

  // 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.");
    }
  }
Enter fullscreen mode Exit fullscreen mode

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.

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();
Enter fullscreen mode Exit fullscreen mode

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.

enum TimerEvents {
  tick,
  timerStart,
  timerStop,
}
Enter fullscreen mode Exit fullscreen mode

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.

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(){}

}
Enter fullscreen mode Exit fullscreen mode

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.

  // 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";
  }
Enter fullscreen mode Exit fullscreen mode

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.

            (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'),
Enter fullscreen mode Exit fullscreen mode

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.

            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,
              ),
            ),
Enter fullscreen mode Exit fullscreen mode

Here are the Start timer and Stop timer buttons code.

            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],
              ),
            ),
Enter fullscreen mode Exit fullscreen mode

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.

            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,
              ),
            ),
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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? πŸ˜ƒ

Buy Me A Coffee

Video

IMAGE ALT TEXT HERE

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

Top comments (0)

DEV

Thank you.

Β 
Thanks for visiting DEV, we’ve worked really hard to cultivate this great community and would love to have you join us. If you’d like to create an account, you can sign up here.