[Tutorial] Asynchronous Flutter chat client with Go chat server which are powered by gRPC (simple and streaming)

I opened Flutter for myself few weeks ago. It dramatically changes mobile app development. I would call it now — development with pleasure 😅.

We are going to implement the following scenario in this article:

  • Mobile chat application allows user to write text messages, then send messages to the server and asynchronously receives echo answers from the server. Mobile chat application is developed with Dart lang and Flutter framework.
  • Chat echo server is developed using Go language. It receives messages from the client via simple gRPC Send method. Server replays echo to the client via gRPC server-side streaming method Subscribe.
  • We are going to use protobuf to define API for client-server communication.

This is how our Flutter chat client application will look:

You can get full source code for this article here:

Prerequisites

  • We have to have installed and configured Go v1.11+ before start. We are going to use Go modules capabilities.
  • We have to install Server SDK for Dart 2. Despite the fact that Flutter includes Dart SDK we need standalone installation to generate Dart files from protobuf API definition.
  • We have to install Flutter v1.0+.
  • We have to install protobuf compiler. See details how to do it here.
  • Next install Go code generator plugin for Proto compiler:
go get -u github.com/golang/protobuf/protoc-gen-go
pub global activate protoc_plugin

Project skeleton

We are going to use the following project structure:

flutter-grpc-tutorial (root folder)
|
|----- api (proto API definition)
|----- flutter_client (chat client App)
|----- go-server (echo chat server)
|----- third_party (miscellaneous files are needed to compile Go and Dart files from proto)

Third party files

third_party folder structure

We need to download empty.proto, timestamp.proto and wrappers.proto from here to the flutter-grpc-tutorial/third_party/google/protobuf folder.

Note: file protoc-gen.cmd is optional — don’t care about it. It is example of proto compilation script for Windows.

Define client-server communication API

api section contains only one file with ChatService definition:

ChatService proto definition

Go chat server

First step is to generate Go protobuf/grpc code from proto API definition. We have to create output folder before:

cd flutter-grpc-tutorial
mkdir -p go-server/pkg/api/v1

Then generate Go code:

protoc chat.proto --proto_path=api/proto/v1 --proto_path=. --go_out=plugins=grpc:go-server/pkg/api/v1

The result of code generation should look like this:

Go lang chat server protobuf generated code

Next we have to create go-server/pkg/service/v1/chat.go file — ChatService implementation. Server receives message from the client and store it to the channel. Subscribe method gets message from channel and sends echo back to the client. Implementation is very simple:

Then let’s develop code to register CharService run gRPC server (go-server/pkg/protocol/grpc/server.go):

And the final step is to develop main() Go application entry point (go-server/cmd/server/main.go):

The result Go chat server project structure looks like this:

Go chat server project structure

Please take full source code for go-server here.

Create Flutter client application project

I have used this official tutorial as entry point of Flutter client for this article:

I strongly recommend you to go through this great lesson. It helps you understand how to develop cool UI app with Flutter.

Note 1: I avoid step “Customize for iOS and Android” to keep code simple.
Note 2: I split main.dart file into multiple files. I prefer several small code files rather than one large file.

You can get source code for our Flutter client application here.

Flutter client application project structure

Before we start let’s open pubspec.yaml file to see the Dart packages we are using. This is the dependencies section:

grpc and protobuf packages gives gRPC engine for Dart language. uuid is used to generate unique ID for chat messages.

First step is to generate Dart protobuf/grpc code from proto API definition. We have to create output folder before:

cd flutter-grpc-tutorial
mkdir -p flutter_client/lib/api/v1/google/protobuf

Then let’s generate Dart code for protobuf support files and for our chat.proto:

protoc empty.proto timestamp.proto wrappers.proto --proto_path=third_party/google/protobuf --plugin=protoc-gen-dart=%USERPROFILE%/AppData/Roaming/Pub/Cache/bin/protoc-gen-dart.bat --dart_out=grpc:flutter_client/lib/api/v1/google/protobuf
protoc chat.proto --proto_path=api/proto/v1 --proto_path=third_party  --plugin=protoc-gen-dart=%USERPROFILE%/AppData/Roaming/Pub/Cache/bin/protoc-gen-dart.bat --dart_out=grpc:flutter_client/lib/api/v1
Note: parameter plugin=protoc-gen-dart=%USERPROFILE%/AppData/Roaming/Pub/Cache/bin/protoc-gen-dart.bat is needed for Windows only. Please ignore this one for MacOS and Linux.

The result of generation should look like this:

Generated Dart files

Great. Let’s take a look at the other Dart files:

main.dart

app.dart

chat_message.dart

This file includes Message class that contains message unique ID and content. ChatMessage is parent base class for both outgoing and incoming chat message widgets.

chat_message_incoming.dart

ChatMessageIncoming is stateless widget is displaying incoming message into the ListView area. Stateless means ChatMessageIncoming object once created never changes anymore.

chat_message_outgoing.dart

ChatMessageOutgoing is stateful widget to display outgoing message into the scrolling ListView area. Stateful means message status can change from UNKNOWN to SENT. ChatMessageOutgoingState state class is used display message considering the status of sending. It draws wait 🕗 icon for UNKNOWN status and done ✔ icon for SENT status.

ChatMessageOutgoingController allows to change message status from outside using setStatus method.

chat_service.dart

Let’s take a look at the some parts of this file.

send method sends message to the server asynchronously. First it creates client connection channel to the server:

Then it sends message to the server using generated from proto file ChatServiceClient stub. In case of success it raises onSentSuccess event with updated (from UNKNOWN to SENT) message status:

In case of error it raises onSentError event, invalidates client connection and try to send again. It stops trying to send a message when application is shutting down:

Next take a look at the startListening method. It creates client connection channel to the server also:

Then it opens gRPC stream using generated from proto file ChatServiceClient stub and starts listening for incoming messages. It raises onReceivedSuccess event when message arrives:

In case of error or closed stream it raises event onReceivedError, invalidate client connection, try to open and listen stream again. It stops trying to listen incoming messages when application is shutting down:

chat_screen.dart

This is the main file to display chat messages. Let’s deep dive into this one and take a look at the some parts. initState method initializes gRPC chat client service and bandwidth buffer (we are going to speak about bandwidth buffer later). It subscribes to the chat service events (onSentSuccess, onSentError, onReceivedSuccess, onReceivedError) and starts listening incoming messages from server:

_handleSubmitted event is raised when you have typed message text and press “send” button. It creates new outgoing message from the input text, displays it through bandwidth buffer and sends message to the server. Send message is asynchronous operation so we do not know result at this stage:

Here is how we process chat client service events. It is very simple code:

Next is part of build method is demonstrating how we display messages on the mobile device screen:

part of ChatScreenState.build method

We use powerful StreamBuilder class to display chat messages by sending data to the stream from several sources. Event handlers _handleSubmitted, onSentSuccess, onReceivedSuccess are the sources.

_addMessages method updates status of existing outgoing message or add new outgoing or incoming message to the message list:

Then ListView builder creates ListView widget with our messages from the result message list:

part of ChatScreenState.build method

So we have only one important subject left - bandwidth buffer. Look at the issue I have created in Flutter GitHub repo:

Thanks to zoechi from the Flutter team for his answer. Based on his recommendation I tried to use debounceBuffer from stream_transform Dart package. It does not work for me unfortunately. That’s why I have developed simple BandwidthBuffer class. It accumulates messages and sends them to StreamBuilder no more than once in the time period you specified in class constructor.

This code creates buffer with 500 milliseconds duration parameter:

part of ChatScreenState.initState method

This is how to send message to the buffer:

This event raises once in 500 milliseconds to send messages to the StreamBuilder via StreamController object:

Here is the code for bandwidth_buffer.dart:

That’s all about code. You can get source code for our Flutter client application here.

Let’s run our chat server and client.

First of all we need to build and run chat server:

cd flutter-grpc-tutorial/go-server/cmd/server
go build .
server

server prints the following log:

2019/01/13 19:42:14 starting gRPC server...

Next we need to run client application. I use Visual Studio Code to run app in debug mode. Here is detail instruction how to setup Visual Studio Code or Android Studio for Flutter development:

So client app is started and we see empty chat screen:

Let’s send couple of messages to the server:

It works!

But this is not the end. Our Flutter chat app is fault-tolerant a little bit. It can work in offline mode. To make sure of this, shutdown the server and type messages again:

Look, our messages are marked as sending. Then start server again and you will see what’s happen. It can spend some time until Dart gRPC client reconnects to the server:

That’s all for now. Full source code is available here.

Thanks!