Comet Code

HomeSearchTagsSnippets

Building a gRPC Server and Client in Dart

#gRPC#Flutter

Caelin Sutch

2020-05-10

Integrating Dart or Flutter with GRPC

gRPC is a modern, open source, and high performance RPC framework that is language agnostic, meaning it can work with a variety of platforms for a variety of use cases. gRPC stands for general-purpose Remote Procedure Calls, which is exactly what it does, provide an easy-to use framework to communicate across platforms or devices using HTTP2 as a transport. gRPC uses protocol buffers, Google's system to serialize structured data. This means that you define your data structures and data accessors in .proto files.

The Project

By the end of this article you'll have a basic todos api built in Dart using gRPC. The API will have three separate service calls, createTodos to create a todo item, readTodos to get all of the current todos, and readTodosStream which will return a stream, think like a socket, of todos which gets constantly updated .

GRPC with Flutter

If you're using gRPC for your Flutter application you're in luck, as gRPC has a supported library for Dart! That means you can make both client and server side code using the gRPC protocol and generate buffers in the Dart language. In this article we'll be building a server and client using gRPC in Dart, but will hold off on integrating with Flutter (though it should be pretty clear how to do so).

Building a gRPC Server in Dart

First thing you're going to want to do is install the Dart SDK. This is seperate from the Flutter SDK and can be installed by following this guide).

Setup your Dart Workspace

Once you Dart installed, you'll want to create a basic Dart workspace.

1mkdir dart-grpc-tutorial
2cd dart-grpc-tutorial && mkdir lib
3touch pubspec.yaml
4touch lib/server.dart

Lets update that pubspec.yaml with the correct information:

pubspec.yaml

1name: grpc_tutorial
2version: 0.0.1
3environment:
4 sdk: ">=2.1.0 <3.0.0"
5dependencies:
6 grpc: 2.1.3
7 protobuf: 1.0.1

and run pub get to install both dependencies.

Now remember what I said about protobuff files? We're going to make a custom protobuff file and compile it for Dart on our own! This proto file is going to hold the definitions for our API.

Installing the Proto Compiler

You'll have to install the proto compiler and the dart protocol buffer plugin to use gRPC.

  1. If you haven't installed the compiler, download the package and follow the instructions in the README.
  2. Install the Dart Protocol Buffer plugin as described in its README. The executable bin/protoc-gen-dart must be in your PATH for the protocol buffer protoc to find it.

Now lets create a folder to hold our protobuff files and the generated dart code, and make our todo.proto file

1mkdir protos && touch protos/todo.proto
2mkdir lib/src && mkdir lib/src/generated

Making your Proto File

Open up the todo.proto file and lets update it with our message type.

todo.proto

1syntax = "proto3";
2
3package todoPackage;
4
5service Todo {
6 rpc createTodo(TodoItem) returns (TodoItem);
7 rpc readTodos(voidNoParam) returns (TodoItems);
8 rpc readTodosStream(voidNoParam) returns (stream TodoItem);
9}
10
11message voidNoParam {}
12
13message TodoItem {
14 int32 id = 1;
15 string text = 2;
16}
17
18message TodoItems {
19 repeated TodoItem items = 1;
20}

Wow that was a lot of code. Lets go through line by line and see what this stuff does.

First we want to declare our proto syntax, which is proto3

1syntax = "proto3";

Next, we declare our package

1package todoPackage;

This is where it gets fun. We declare our actual TodoService here. You can think of the service like an API specification, it lays out the different "endpoints" of the API.

1service Todo {
2 rpc createTodo(TodoItem) returns (TodoItem); // Add a todo
3 rpc readTodos(voidNoParam) returns (TodoItems); // Get all todos
4 rpc readTodosStream(voidNoParam) returns (stream TodoItem); // Get all todos as a stream
5}

You'll notice that we there are types here, lets break these down

1rpc createTodo(TodoItem) returns (TodoItem)

The RPC in front declares it as a rpc endpoint. The name of the endpoint, createTodo is declared, as well as the input parameter, which is a TodoItem. TodoItem is declared as a message type on the bottom of this block of code, we'll go over it soon. Finally returns (TodoItem) tells us the return type of the function, which is a TodoItem as well.

1rpc readTodos(voidNoParam) returns (TodoItems);

This one is interesting because you'll see we pass in a type called voidNoParam. In gRPC, you can't have an empty parameter input, so we pass this in, which is just an empty object, to prevent errors.

1rpc readTodosStream(voidNoParam) returns (stream TodoItem);

Finally, this one returns a stream of TodoItems. Coming from a Dart background, you should already know how a stream behaves, as a constantly updating set of information.

The message types are then declared below. Notice that each parameter of each message type is declared with a type:

1message TodoItem {
2 int32 id = 1;
3 string text = 2;
4}

The number afterward tell gRPC what order to expect these parameters in. The repeated type that you see in the TodoItems just means an Array (or List) or TodoItems.

Compile Your Proto File

Now we'll compile the proto file into usable Dart code by running

1protoc -I protos/ protos/todo.proto --dart_out=grpc:lib/src/generated

If everything went correctly, you should see your generated folder fill up with some code files.

Write A Simple Server

I've commented what each part of this code does, but essentially we're using the generated code from the proto file to create the TodoService. We create a class TodoServer to handle the creation and implementation of the gRPC server using TodoService. This is one of the advantages of having the proto files, they offer easily expandable abstract classes to implement the methods.

server.dart

1import 'package:grpc/grpc.dart'; // Import GRPC
2import 'package:grpc/src/server/call.dart'; // Import GRPC Server
3import 'package:grpc_tutorial/src/generated/todo.pbgrpc.dart'; // Import the protofbuff files
4
5class TodoService extends TodoServiceBase {
6 TodoItems todos = new TodoItems(); // TodoItems is what's generated by Proto
7
8 /**
9 * Createa new Todo
10 * @param call - meta information on the request
11 * @param request - information that's sent
12 * @returns todoItem - The item that was added
13 **/
14 @override
15 Future<TodoItem> createTodo(ServiceCall call, TodoItem request) async {
16 // Create a new TodoObject with the data
17 TodoItem todoItem = new TodoItem();
18 todoItem.text = request.text;
19 todoItem.id = todos.$_getList(1).length +1;
20 // Add a new todo Item to our list
21 todos.items.add(todoItem);
22 // Return a todoItem in accordance with function return typ[e
23 return todoItem;
24 }
25 /**
26 * Read the todos as a Future
27 **/
28 @override
29 Future<TodoItems> readTodos(ServiceCall call, voidNoParam request) async {
30 return todos;
31 }
32
33 /**
34 * Get all of the todos as a stream
35 **/
36 @override
37 Stream<TodoItem> readTodosStream(ServiceCall call, voidNoParam request) async* {
38 // Iterate through all of the todos and 'yield' each todo (returns it to the stream)
39 for (var todo in todos.items) {
40 yield todo;
41 }
42 }
43}
44
45/**
46* gRPC Server
47**/
48class TodoServer {
49 Future<void> main(List<String> args) async {
50 final server = Server([TodoService()]); // Create a new server from the TodoService
51 await server.serve(port: 9000); // Start the server on port 9000
52 print('Server listening on port ${server.port}...');
53 }}
54
55
56main() {
57 TodoServer todoServer = new TodoServer();
58 todoServer.main([]);
59}

Write a Simple Client

Now that you have a server, lets write a client. I'll use inline comments to explain what this code is doing.

client.dart

1import 'package:grpc/grpc.dart';
2import 'package:grpc_tutorial/src/generated/todo.pbgrpc.dart'; // Import GRPC
3
4// Helper class to handle client methods. In the real world, you'd probably have wrappers aroudn each gRPC method call
5class Client {
6 ClientChannel channel;
7 TodoClient stub;
8
9 Future<void> main(List<String> args) async {
10 // Create a new channel with localhost and the server port
11 channel = ClientChannel('localhost',
12 port: 8001,
13 options: // No credentials in this example
14 const ChannelOptions(credentials: ChannelCredentials.insecure()));
15 // Create the stub, which is the client that you interact with to get gRPC methods
16 stub = TodoClient(channel,
17 options: CallOptions(timeout: Duration(seconds: 30)));
18 try {
19 // Create a new todo
20 final TodoItem todoItem = new TodoItem();
21 todoItem.text = "Test 1";
22 // Send a request to the server to make the new item
23 var res = await stub.createTodo(todoItem);
24 // Prints the recieved item
25 print(res);
26
27 // Create the void message
28 final v = new voidNoParam();
29 // Get a list of todos as a future
30 var todos = await stub.readTodos(v);
31 print(todos.items);
32
33 // Get the todos as a stream, listen to it, and print the values. If you run another client and create new todos, you'll see this stream get updated
34 stub.readTodosStream(v).listen((value) {print(value); });
35 } catch (e) {
36 print(e);
37 }
38 // Cleanup with shutdown
39 await channel.shutdown();
40 }}
41
42main() {
43 var client = Client();
44 client.main([]);
45}

Bam! That's it, you just created your first gRPC server and client exclusively in Dart. To integrate this in Flutter, you'd simply write more methods in the Client class, and then use it in widgets to add and get data (StreamBuilder is your friend).

Caelin Sutch ©2020