In this blog we're going to build, yup you guessed it, a Flutter counter — but we're not going to build just any Flutter counter. We're going to build a Flutter counter which connects to a Dart Frog server using WebSockets and increment the value of the counter in real time across all platforms! When we're finished, we should have something that looks like this:
We'll learn how to establish a WebSocket connection, react to changes in the connection state, and send and receive messages from the server. In this blog, we will focus on the Flutter application. Before we dive in, check out the Dart Frog WebSocket Counter tutorial in the Dart Frog docs to understand how to build the server used in this example.
First, let's take a look at a how the app will work:
The Flutter application we'll build in this blog will establish a WebSocket connection with a Dart Frog server and listen for changes in count. When a user taps the increment or decrement buttons, the client will send an increment or decrement message to the Dart Frog server. The Dart Frog server will handle computing the new count and broadcast that count to all connected clients.
When we're all done, our project structure should look something like this:
Note that other directories like ios, android, etc. are omitted for simplicity.
Creating the Flutter app
Let's use very_good_cli to generate a new Flutter application. If you haven't used very_good_cli already, you can install it from pub.dev:
Then, let's use the create command to generate a new Flutter app:
At this point we have a Flutter starter application with VGV best practices baked in. To learn more about Very Good CLI, check out the official documentation site.
Before we make changes to the Flutter application, we'll start by creating a counter_repository package which will manage the counter domain. Creating the counter_repository allows us to establish a layer of abstraction between the application and the data giving us flexibility. For example, in the future if we decide to move away from WebSockets, we will not need to modify the Flutter application. To learn more about a layered architecture, check out our blog post.
We can use very_good_cli to create the counter_repository package:
Next, we'll need to add two dependencies to the counter_repository pubspec.yaml:
- web_socket_client: A Dart WebSocket Client which will allow us to communicate with the Dart Frog server.
- web_socket_counter: The Dart Frog WebSocket counter package which exports models which we can reuse.
The pubspec.yaml should look like:
Then, install the dependencies by running dart pub get.
The CounterRepository class will expose public methods that abstract the implementation details and provide domain models for the application to consume.
We are exposing increment and decrement methods which send messages to the Dart Frog server. Note that the Message enum allows us to use strongly typed objects, and it is used and exported by the Dart Frog server. Being able to share the Message enum is an example of code sharing, which can help you be more efficient when building full stack Dart applications and ensure the frontend client code is compatible with the server.
The CounterRepository also exposes a stream of counts which the application can subscribe to in order to receive updates. A stream of ConnectionState is exposed in order to allow the application to react to changes in the WebSocket connection.
Lastly, the CounterRepository exposes a close method which closes the underlying connection and can be used to free up any resources.
That's it for the counter_repository implementation! Next, add the counter_repository to the pubspec.yaml of the Flutter application.
The updated pubspec.yaml should look like:
Make sure to install all dependencies by running flutter packages get.
Now time to switch gears and focus on the Flutter app!
The application generated by Very Good CLI already comes with a counter feature which you can find in the lib directory. The first thing we'll do is remove the cubit directory and create a CounterBloc instead. Since we'll be working with streams, using a bloc provides some advantages over a cubit.
Once we have generated a new CounterBloc, let's work on modeling the state. In this example, we'll model the state as a single object which contains the value of the count as well as a status to indicate whether the client is connected to the server:
Next, let's define the events the bloc will be reacting to.
The CounterBloc in this example will react to five different events but only three of those events will be external.
- CounterStarted: notifies the bloc that the counter feature has started and prompts the bloc to subscribe to changes from the backend
- CounterIncrementPressed: notifies the bloc that the user has tapped on the increment button
- CounterDecrementPressed: notifies the bloc that the user has tapped on the decrement button
- _CounterCountChanged: notifies the bloc that the count has changed on the backend (internal)
- _CounterConnectionStateChanged: notifies the bloc that the connection state has changed (internal)
Now that we have the state and events defined, we can implement the bloc.
First, the bloc will depend on the CounterRepository we created earlier, so we'll add it as a required parameter to the bloc constructor.
Next, we'll create an event handler for each event. Tip: you can use the onevent and _onevent snippets from the Bloc VS Code extension.
Let's start with CounterStarted. When the bloc receives a CounterStarted event, it will subscribe to changes in the count and connection and add internal events for whenever either changes. As a result, we need to define two StreamSubscription instances and override close to cancel those subscriptions when the bloc is closed.
Next, let's implement the _CounterConnectionStateChanged handler. When the connection state changes, the bloc will emit a new state with an updated status based on whether we are connected or disconnected from the backend. We use a private extension on ConnectionState to convert the ConnectionState to a CounterStatus.
The _CounterCountChanged event handler will be very similar so let's implement that next. When the bloc receives a _CounterCountChanged event it will emit a new state with the updated count and in this case always set the status to connected since the bloc can only ever receive count updates from the server if it is connected.
The CounterIncrementPressed and CounterDecrementPressed event handlers are all that's left and when a bloc receives either event it will invoke increment or decrement on the CounterRepository respectively.
The finished CounterBloc implementation should look like:
Now that the CounterBloc is implemented, let's update the UI. First, let's provide an instance of the CounterRepository to the widget tree in app.dart
Note that we're using a StatefulWidget so that we can manage the CounterRepository instance and close it when the widget is disposed.
Next, let's update the CounterPage widget to provide an instance of the CounterBloc.
We immediately add a CounterStarted event to the bloc in order to start listening for updates to the count as soon the widget is mounted.
Next, let's update the CounterView to display the counter text, connection text, and floating action buttons to increment and decrement the counter:
The CounterText widget will use the context.select extension to rebuild whenever the count changes in the bloc state:
Similarly, the ConnectionText widget uses context.select to rebuild whenever the status changes in the bloc state:
Lastly, the increment and decrement buttons are only enabled if the status is connected. When tapped, they add the correct event to the bloc:
Running the App
We're finally ready to run the app now! We recommend running the app in debug mode directly from either VS Code or Android Studio but you can also run it from the command line:
Once the app is running you should see the client is disconnected and the floating action buttons are disabled.
Now all that's left to do is run the Dart Frog server. To see how we built the server, refer to the tutorial in the Dart Frog docs.
Once the server is running you should see the client establish a connection. You can run the Flutter app on a different device to establish multiple connections to the server and watch the counter change in real-time.
That's it! You can now build a full stack Dart application with real-time bidirectional communication using WebSockets. The complete source code is available on GitHub (with 100% test coverage, of course).