WebSocket Counter 🔌
Difficulty: 🟠 Intermediate
Length: 30 minutes
Before getting started, read the Dart Frog prerequisites to make sure your development environment is ready.
Overview
In this tutorial, we're going to build an app that exposes a single endpoint which handles WebSocket connections and maintains a real-time counter which can be incremented and decremented by connected clients.
When we're done, we should be able to connect to the /ws
endpoint and send or receive messages.
import 'package:web_socket_channel/web_socket_channel.dart';
void main() async {
final channel = WebSocketChannel.connect(Uri.parse('ws://localhost:8080/ws'));
channel.stream.listen(print);
channel.sink.add('__increment__');
channel.sink.add('__decrement__');
channel.sink.close();
}
We should see the following output:
0 # initial
1 # increment
0 # decrement
Creating a new app
To create a new Dart Frog app, open your terminal, change to the directory where you'd like to create the app, and run the following command:
dart_frog create web_socket_counter
You should see an output similar to:
✓ Creating web_socket_counter (0.1s)
✓ Installing dependencies (1.7s)
Created web_socket_counter at ./web_socket_counter.
Get started by typing:
cd ./web_socket_counter
dart_frog dev
Install and use the Dart Frog VS Code extension to easily create Dart Frog apps within your IDE.
Running the development server
You should now have a directory called web_socket_counter
. Let's change directories into the newly created project:
cd web_socket_counter
Then, run the following command:
dart_frog dev
This will start the development server on port 8080
:
✓ Running on http://localhost:8080 (1.3s)
The Dart VM service is listening on http://127.0.0.1:8181/YKEF_nbwOpM=/
The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8181/YKEF_nbwOpM=/devtools/#/?uri=ws%3A%2F%2F127.0.0.1%3A8181%2FYKEF_nbwOpM%3D%2Fws
[hotreload] Hot reload is enabled.
Make sure it's working by opening http://localhost:8080 in your browser or via cURL
:
curl --request GET \
--url http://localhost:8080
If everything succeeded, you should see Welcome to Dart Frog!
.
Creating the WebSocket Route
Now that we have a running application, let's start by creating a new ws
route at routes/ws.dart
:
import 'package:dart_frog/dart_frog.dart';
Response onRequest(RequestContext context) {
return Response(body: 'You have requested /ws');
}
We can also delete the root endpoint at routes/index.dart
since we won't be needing it for this example.
Install and use the Dart Frog VS Code extension to easily create new routes within your IDE.
Save the changes and hot reload should kick in ⚡️
[hotreload] - Application reloaded.
Now if we visit http://localhost:8080/ws in the browser or via cURL
:
curl --request GET \
--url http://localhost:8080/ws
We should see our new response:
You have requested /ws
Adding a WebSocket Handler
Next, we need to upgrade our route handler to handle WebSocket connections. To do this we'll use the dart_frog_web_socket package.
Add the dart_frog_web_socket
dependency:
dart pub add dart_frog_web_socket
Now, let's update our route handler at routes/ws.dart
to use the provided webSocketHandler
from dart_frog_web_socket
:
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_web_socket/dart_frog_web_socket.dart';
Future<Response> onRequest(RequestContext context) async {
final handler = webSocketHandler(
(channel, protocol) {
// A new client has connected to our server.
print('connected');
// Send a message to the client.
channel.sink.add('hello from the server');
// Listen for messages from the client.
channel.stream.listen(
print,
// The client has disconnected.
onDone: () => print('disconnected'),
);
},
);
return handler(context);
}
For more information, refer to the WebSocket documentation.
Save the changes and hot reload should kick in ⚡️
Now we should be able to write a simple script to test the WebSocket connection.
Establishing a WebSocket Connection
Create a new directory called example
at the project root and create a pubspec.yaml
:
name: example
publish_to: none
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
web_socket_channel: ^2.4.0
Next, install the dependencies:
dart pub get
Now, create a main.dart
with the following contents:
import 'package:web_socket_channel/web_socket_channel.dart';
void main() {
// Connect to the remote WebSocket endpoint.
final uri = Uri.parse('ws://localhost:8080/ws');
final channel = WebSocketChannel.connect(uri);
// Listen to messages from the server.
channel.stream.listen(print);
// Send a message to the server.
channel.sink.add('hello from the client');
// Close the connection.
channel.sink.close();
}
We're using package:web_socket_channel
to connect to our Dart Frog /ws
endpoint. We can send messages to the server by calling add
on the WebSocketChannel
sink. We can listen to incoming messages by subscribing to the WebSocketChannel
stream.
With the Dart Frog server still running, open a separate terminal, and run the example script:
dart example/main.dart
We should see the following output on the client:
hello from the server
On the server we should see the following output:
connected
hello from the client
disconnected
Awesome! We've configured a WebSocket handler and established a connection to our server 🎉
Managing the Counter State
Now that we've configured the WebSocket handler, we're going to shift gears and work on creating a component that will manage the state of the counter.
In this example, we're going to use a cubit from the Bloc Library to manage the state of our counter because it provides a reactive API which allows us to stream state changes and query the current state at any given point in time. We're going to use package:broadcast_bloc which allows blocs or cubits to broadcast their state changes to any subscribed stream channels — this will come in handy later on.
Let's add the broadcast_bloc
dependency:
dart pub add broadcast_bloc
Then, create a cubit in lib/counter/cubit/counter_cubit.dart
.
import 'package:broadcast_bloc/broadcast_bloc.dart';
class CounterCubit extends BroadcastCubit<int> {
// Create an instance with an initial state of 0.
CounterCubit() : super(0);
// Increment the current state.
void increment() => emit(state + 1);
// Decrement the current state.
void decrement() => emit(state - 1);
}
In order to access the cubit from our route handler, we'll create a provider
in lib/counter/middleware/counter_provider.dart
.
import 'package:dart_frog/dart_frog.dart';
import 'package:web_socket_counter/counter/counter.dart';
final _counter = CounterCubit();
// Provide the counter instance via `RequestContext`.
final counterProvider = provider<CounterCubit>((_) => _counter);
For more information, refer to the dependency injection documentation.
Let's also create a barrel file which exports all counter
components in lib/counter/counter.dart
:
export 'cubit/counter_cubit.dart';
export 'middleware/counter_provider.dart';
Providing the Counter
We need to use the counterProvider
in order to have access to it in nested. Create a global piece of middleware (routes/_middleware.dart
):
import 'package:dart_frog/dart_frog.dart';
import 'package:web_socket_counter/counter/counter.dart';
Handler middleware(Handler handler) => handler.use(counterProvider);
For more information, refer to the middleware documentation.
Install and use the Dart Frog VS Code extension to easily create new middleware within your IDE.
Using the Counter
We can access the CounterCubit
instance from our WebSocket handler via context.read<CounterCubit>()
.
import 'package:dart_frog/dart_frog.dart';
import 'package:dart_frog_web_socket/dart_frog_web_socket.dart';
import 'package:web_socket_counter/counter/counter.dart';
Future<Response> onRequest(RequestContext context) async {
final handler = webSocketHandler(
(channel, protocol) {
// A new client has connected to our server.
// Subscribe the new client to receive notifications
// whenever the cubit state changes.
final cubit = context.read<CounterCubit>()..subscribe(channel);
// Send the current count to the new client.
channel.sink.add('${cubit.state}');
// Listen for messages from the client.
channel.stream.listen(
(event) {
switch (event) {
// Handle an increment message.
case '__increment__':
cubit.increment();
break;
// Handle a decrement message.
case '__decrement__':
cubit.decrement();
break;
// Ignore any other messages.
default:
break;
}
},
// The client has disconnected.
// Unsubscribe the channel.
onDone: () => cubit.unsubscribe(channel),
);
},
);
return handler(context);
}
First, we subscribe the newly connected client to the CounterCubit
in order to receive updates whenever the cubit state changes.
Next, we send the current count to the new client via cubit.state
.
When the client sends a new message, we invoke the increment
/decrement
method on the cubit based on the message.
Finally, we unsubscribe the channel when the client disconnects.
The subscribe
and unsubscribe
APIs are exposed by the BroadcastCubit
super class from package:broadcast_bloc
.
Be sure to save all the changes and hot reload should kick in ⚡️
[hotreload] - Application reloaded.
Now we can update our example script in example/main.dart
:
import 'package:web_socket_channel/web_socket_channel.dart';
void main() async {
final channel = WebSocketChannel.connect(Uri.parse('ws://localhost:8080/ws'));
channel.stream.listen(print);
channel.sink.add('__increment__');
channel.sink.add('__decrement__');
channel.sink.close();
}
Finally, let's run the script:
dart example/main.dart
We should see the following output:
0
1
0
If you restart the server, the count will always be reset to 0 because it is only maintained in memory.
🎉 Congrats, you've created a real-time counter application using Dart Frog. View the full source code.