# Dart Frog full stack tutorial

> Build a real-time Flutter app using WebSockets and Dart Frog

- Source: https://verygood.ventures/blog/dart-frog-full-stack-tutorial/
- Published: 2023-01-11
- Author: VGV Team
- Tags: Backend, Open Source, Tutorial

---

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](https://dart-frog.dev/tutorials/web-socket-counter/) in the Dart Frog docs to understand how to build the server used in this example.

## Project structure

First, let's take a look at a how the app will work:

![Diagram of project structure with steps 1. Send increment message 2. Compute new count 3. Broadcast new count. Diagram has three devices displaying a Flutter app. Above is a cloud with the Dart Frog logo to represent the Dart Frog server. An increment event arrow extends from one device to the Dart Frog server. Arrows point from the Dart Frog server to the three devices to represent the updated count on all devices.](/assets/images/blog/dart-frog-full-stack-tutorial/body-0.png)

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:

```txt
├── lib
│   ├── app
│   │   └── view
│   ├── counter
│   │   ├── bloc
│   │   └── view
│   └── l10n
├── packages
│   └── counter_repository
├── test
```

Note that other directories like **ios**, **android**, etc. are omitted for simplicity.

## Creating the Flutter app

Let's use [**very_good_cli**](https://pub.dev/packages/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:

```shell
dart pub global activate very_good_cli
```

Then, let's use the create command to generate a new Flutter app:

```shell
very_good create web_socket_counter_flutter --desc "A Flutter real-time counter which integrates with Dart Frog and WebSockets."
```

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](https://cli.vgv.dev/).

### Counter Repository

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](https://verygood.ventures/blog/very-good-flutter-architecture/).

We can use **very\_good\_cli** to create the **counter\_repository** package:

```shell
very_good create counter_repository -t dart_pkg --desc "A Dart package which manages the counter domain."
```

Next, we'll need to add two dependencies to the **counter\_repository pubspec.yaml**:

-   [**web_socket_client**](https://pub.dev/packages/web_socket_client): A Dart WebSocket Client which will allow us to communicate with the Dart Frog server.
-   [**web_socket_counter**](https://github.com/VeryGoodOpenSource/dart_frog/tree/main/examples/web_socket_counter): The Dart Frog WebSocket counter package which exports models which we can reuse.

The **pubspec.yaml** should look like:

```yaml
name: counter_repository
description: A Dart package which manages the counter domain.
version: 0.1.0+1
publish_to: none
environment:
  sdk: ">=2.18.0 <3.0.0"
dependencies:
  web_socket_client: ^0.1.0-dev.1
  web_socket_counter:
    git:
      url: https://github.com/VeryGoodOpenSource/dart_frog
      path: examples/web_socket_counter
dev_dependencies:
  mocktail: ^0.3.0
  test: ^1.19.2
  very_good_analysis: ^3.1.0
```

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.

```dart
import 'package:web_socket_client/web_socket_client.dart';
import 'package:web_socket_counter/counter/counter.dart';

/// A Dart package which manages the counter domain.
class CounterRepository {
  CounterRepository({WebSocket? socket})
      : _ws = socket ?? WebSocket(Uri.parse('ws://localhost:8080/ws'));

  final WebSocket _ws;

  /// Send an increment message to the server.
  void increment() => _ws.send(Message.increment.value);

  /// Send a decrement message to the server.
  void decrement() => _ws.send(Message.decrement.value);

  /// Return a stream of real-time count updates from the server.
  Stream get count => _ws.messages.cast().map(int.parse);

  /// Return a stream of connection updates from the server.
  Stream get connection => _ws.connection;

  /// Close the connection.
  void close() => _ws.close();
}
```

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:

```yaml
name: web_socket_counter_flutter
description: A Flutter real-time counter which integrates with Dart Frog and WebSockets.
version: 1.0.0+1
publish_to: none
environment:
  sdk: ">=2.18.0 <3.0.0"
  flutter: 3.3.10
dependencies:
  bloc: ^8.1.0
  counter_repository:
    path: packages/counter_repository
  equatable: ^2.0.5
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.1
  flutter_localizations:
    sdk: flutter
  intl: ^0.17.0
dev_dependencies:
  bloc_test: ^9.1.0
  flutter_test:
    sdk: flutter
  mocktail: ^0.3.0
  very_good_analysis: ^3.1.0
flutter:
  uses-material-design: true
  generate: true
```

Make sure to install all dependencies by running **flutter packages get**.

Now time to switch gears and focus on the Flutter app!

### Counter Bloc

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](https://bloclibrary.dev/#/coreconcepts?id=cubit-vs-bloc).

We can create a new bloc either by using the [bloc VS Code extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) or using the [bloc brick](https://brickhub.dev/bricks/bloc/0.2.0).

**CounterState**

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:

```dart
part of 'counter_bloc.dart';

enum CounterStatus { connected, disconnected }

class CounterState extends Equatable {
  const CounterState({
    this.count = 0,
    this.status = CounterStatus.disconnected,
  });

  final int count;
  final CounterStatus status;

  @override
  List get props => [count, status];

  CounterState copyWith({int? count, CounterStatus? status}) {
    return CounterState(
      count: count ?? this.count,
      status: status ?? this.status,
    );
  }
}
```

**CounterEvent**

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)


```dart
part of 'counter_bloc.dart';

abstract class CounterEvent {
  const CounterEvent();
}

class CounterStarted extends CounterEvent {
  const CounterStarted();
}

class CounterIncrementPressed extends CounterEvent {
  const CounterIncrementPressed();
}

class CounterDecrementPressed extends CounterEvent {
  const CounterDecrementPressed();
}

class _CounterCountChanged extends CounterEvent {
  const _CounterCountChanged(this.count);

  final int count;
}

class _CounterConnectionStateChanged extends CounterEvent {
  const _CounterConnectionStateChanged(this.state);

  final ConnectionState state;
}
```

**CounterBloc**

Now that we have the state and events defined, we can implement the bloc.

```dart
class CounterBloc extends Bloc {
  CounterBloc({required CounterRepository counterRepository})
      : _counterRepository = counterRepository,
        super(const CounterState()) {...}

  final CounterRepository _counterRepository;
}
```

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.

```dart
class CounterBloc extends Bloc {
  CounterBloc({required CounterRepository counterRepository})
      : _counterRepository = counterRepository,
        super(const CounterState()) {
    on(_onCounterStarted);
  }

  StreamSubscription? _countSubscription;
  StreamSubscription? _connectionSubscription;

  void _onCounterStarted(
    CounterStarted event,
    Emitter emit,
  ) {
    _countSubscription = _counterRepository.count.listen(
      (count) => add(_CounterCountChanged(count)),
    );
    _connectionSubscription = _counterRepository.connection.listen((state) {
      add(_CounterConnectionStateChanged(state));
    });
  }

  @override
  Future close() {
    _connectionSubscription?.cancel();
    _countSubscription?.cancel();
    return super.close();
  }
}
```

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**.

```dart
class CounterBloc extends Bloc {
  CounterBloc({required CounterRepository counterRepository})
      : _counterRepository = counterRepository,
        super(const CounterState()) {
    ...
    on<_CounterConnectionStateChanged>(_onCounterConnectionStateChanged);
  }

  ...

  void _onCounterConnectionStateChanged(
    _CounterConnectionStateChanged event,
    Emitter emit,
  ) {
    emit(state.copyWith(status: event.state.toStatus()));
  }
}

extension on ConnectionState {
  CounterStatus toStatus() {
    return this is Connected || this is Reconnected
        ? CounterStatus.connected
        : CounterStatus.disconnected;
  }
}
```

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.

```dart
class CounterBloc extends Bloc {
  CounterBloc({required CounterRepository counterRepository})
      : _counterRepository = counterRepository,
        super(const CounterState()) {
    on(_onCounterStarted);
    on<_CounterConnectionStateChanged>(_onCounterConnectionStateChanged);
    on<_CounterCountChanged>(_onCounterCountChanged);
  }

  ...

  void _onCounterCountChanged(
    _CounterCountChanged event,
    Emitter emit,
  ) {
    emit(state.copyWith(count: event.count, status: CounterStatus.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:

```dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:counter_repository/counter_repository.dart';
import 'package:equatable/equatable.dart';

part 'counter_event.dart';
part 'counter_state.dart';

class CounterBloc extends Bloc {
  CounterBloc({required CounterRepository counterRepository})
      : _counterRepository = counterRepository,
        super(const CounterState()) {
    on(_onCounterStarted);
    on<_CounterConnectionStateChanged>(_onCounterConnectionStateChanged);
    on<_CounterCountChanged>(_onCounterCountChanged);
    on(_onCounterIncrementPressed);
    on(_onCounterDecrementPressed);
  }

  final CounterRepository _counterRepository;
  StreamSubscription? _countSubscription;
  StreamSubscription? _connectionSubscription;

  void _onCounterStarted(
    CounterStarted event,
    Emitter emit,
  ) {
    _countSubscription = _counterRepository.count.listen(
      (count) => add(_CounterCountChanged(count)),
    );
    _connectionSubscription = _counterRepository.connection.listen((state) {
      add(_CounterConnectionStateChanged(state));
    });
  }

  void _onCounterIncrementPressed(
    CounterIncrementPressed event,
    Emitter emit,
  ) {
    _counterRepository.increment();
  }

  void _onCounterDecrementPressed(
    CounterDecrementPressed event,
    Emitter emit,
  ) {
    _counterRepository.decrement();
  }

  void _onCounterConnectionStateChanged(
    _CounterConnectionStateChanged event,
    Emitter emit,
  ) {
    emit(state.copyWith(status: event.state.toStatus()));
  }

  void _onCounterCountChanged(
    _CounterCountChanged event,
    Emitter emit,
  ) {
    emit(state.copyWith(count: event.count, status: CounterStatus.connected));
  }

  @override
  Future close() {
    _connectionSubscription?.cancel();
    _countSubscription?.cancel();
    return super.close();
  }
}

extension on ConnectionState {
  CounterStatus toStatus() {
    return this is Connected || this is Reconnected
        ? CounterStatus.connected
        : CounterStatus.disconnected;
  }
}
```

### Counter Feature

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**

```dart
import 'package:counter_repository/counter_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:web_socket_counter_flutter/counter/counter.dart';
import 'package:web_socket_counter_flutter/l10n/l10n.dart';

class App extends StatefulWidget {
  const App({super.key});

  @override
  State createState() => _AppState();
}

class _AppState extends State {
  late final CounterRepository _counterRepository;

  @override
  void initState() {
    super.initState();
    _counterRepository = CounterRepository();
  }

  @override
  void dispose() {
    _counterRepository.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RepositoryProvider.value(
      value: _counterRepository,
      child: const AppView(),
    );
  }
}

class AppView extends StatelessWidget {
  const AppView({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: CounterPage(),
    );
  }
}
```

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**.

```dart
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(
        counterRepository: context.read(),
      )..add(const CounterStarted()),
      child: const CounterView(),
    );
  }
}
```

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:

```dart
class CounterView extends StatelessWidget {
  const CounterView({super.key});

  @override
  Widget build(BuildContext context) {
    final l10n = context.l10n;
    return Scaffold(
      appBar: AppBar(title: Text(l10n.counterAppBarTitle)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const [
            CounterText(),
            ConnectionText(),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: const [
          IncrementButton(),
          SizedBox(height: 8),
          DecrementButton(),
        ],
      ),
    );
  }
}
```

The **CounterText** widget will use the **context.select** extension to rebuild whenever the count changes in the bloc state:

```dart
class CounterText extends StatelessWidget {
  const CounterText({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final count = context.select((CounterBloc bloc) => bloc.state.count);
    return Text('$count', style: theme.textTheme.headline1);
  }
}
```

Similarly, the **ConnectionText** widget uses **context.select** to rebuild whenever the status changes in the bloc state:

```dart
class ConnectionText extends StatelessWidget {
  const ConnectionText({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l10n = context.l10n;
    final connection = context.select((CounterBloc bloc) => bloc.state.status);
    switch (connection) {
      case CounterStatus.connected:
        return Text(
          l10n.counterConnectedText,
          style: theme.textTheme.caption?.copyWith(
            color: theme.colorScheme.primary,
          ),
        );
      case CounterStatus.disconnected:
        return Text(
          l10n.counterDisconnectedText,
          style: theme.textTheme.caption?.copyWith(
            color: theme.colorScheme.error,
          ),
        );
    }
  }
```

Lastly, the increment and decrement buttons are only enabled if the status is connected. When tapped, they add the correct event to the bloc:

```dart
class IncrementButton extends StatelessWidget {
  const IncrementButton({super.key});

  @override
  Widget build(BuildContext context) {
    final isConnected = context.select(
      (CounterBloc bloc) => bloc.state.status == CounterStatus.connected,
    );
    return FloatingActionButton(
      backgroundColor: isConnected ? null : Colors.grey,
      onPressed: isConnected
          ? () => context.read().add(const CounterIncrementPressed());
          : null,
      child: const Icon(Icons.add),
    );
  }
}

class DecrementButton extends StatelessWidget {
  const DecrementButton({super.key});

  @override
  Widget build(BuildContext context) {
    final isConnected = context.select(
      (CounterBloc bloc) => bloc.state.status == CounterStatus.connected,
    );
    return FloatingActionButton(
      backgroundColor: isConnected ? null : Colors.grey,
      onPressed: isConnected
          ? () => context.read().add(const CounterDecrementPressed());
          : null,
      child: const Icon(Icons.remove),
    );
  }
}
```

### 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:

```shell
flutter run -t lib/main_development.dart --flavor development
```

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](https://dart-frog.dev/tutorials/web-socket-counter/) in the Dart Frog docs.

```shell
# Install Dart Frog CLI.
dart pub global activate dart_frog_cli

# Clone the GitHub Repository.
git clone https://github.com/VeryGoodOpenSource/dart_frog

# Change into the web_socket_counter example directory.
cd dart_frog/examples/web_socket_counter

# Install the dependencies.
dart pub get

# Run the development server.
dart_frog dev
```

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.

## Summary

That's it! You can now build a full stack Dart application with real-time bidirectional communication using WebSockets. The [complete source code](https://github.com/VGVentures/web_socket_counter_flutter) is available on GitHub (with 100% test coverage, of course).

### [**Learn more about Dart Frog at dart-frog.dev →**](https://dart-frog.dev/)
