If you need only the code you can check this repo 👇

GitHub - flutterwtf/Flutter-gRPC-Sample
Contribute to flutterwtf/Flutter-gRPC-Sample development by creating an account on GitHub.

What is gRPC?

gRPC is a modern open-source RPC (Remote Procedure Call) framework from Google. It can run in any environment and allows different services to communicate between each other in a very convenient and performant way. With this framework you can forget about disadvantages of HTTP 1.1, low performance of JSON and “eternal” choosing which HTTP-method to use. It also supports 14 programming languages, is platform independent and has built-in code generation. Sounds nice? Let’s talk about all of that in more detail.

Basics and Benefits of gRPC

Protocol Buffers

Currently JSON is the most popular data-interchange format. It’s human-readable and can work with any platform or language. But it has 1 disadvantage — in some cases it’s too slow.

In gRPC this problem is solved. Unlike REST, gRPC uses Protocol Buffers (later Protobuf) as a data-interchange format. Like JSON, it is language independent and platform neutral, but is way faster and more efficient.

Protobuf also supports more data types than JSON (for example enums and functions which the latter doesn`t support). Protobuf messages are in a binary format. These messages include not just the message itself but also the set of rules and tools to define and exchange them.

HTTP 2.0

Using HTTP/2 is another reason why gRPC is that performant.

The key difference between HTTP/2 and HTTP/1.1 is the binary framing layer.

In a traditional HTTP protocol, it is not possible to send multiple requests or get multiple responses together in a single connection. A new connection will need to be created for each of them. This kind of request/response multiplexing is made possible in HTTP/2 due to the binary framing layer.

So for us, developers, this means that we can still handle basic client-server requests just like in HTTP/1.1, when client sends one request and server sends one response. But we can also open long-time connections between client and server which allows us to do much more things in a single request.

Streaming

Streaming is one of the main concepts of gRPC. Like mentioned above, this approach allows us to send or/and receive several messages within one request.

In gRPC, there are 3 types of streaming:

  • Server-side streaming: when the client sends a single request and the server can send back multiple responses (stream of responses);
  • Client-side streaming: when the client sends multiple requests (stream of requests) and the server only sends back a single response;
  • Bidirectional streaming: when both the client and server send messages to each other at the same time without waiting for a response.

Code Generation

gRPC has built-in code generation functionality, so there is no need for you to use third-party tools like Swagger. Protobuf compiler (supplied by gRPC) is a command-line tool for compiling the IDL code in the .proto files and generating specified language source code for it. It is compatible with lots of programming language (such as Java, C#, Dart and many others).

This makes gRPC a great technology for multi-language environments where you connect many different microservices written in different languages.

How to Use

Now that we have knowledge about the basics of gRPC let`s learn how to use it.

As an example I will take this project on GitHub:

GitHub - flutterwtf/Flutter-gRPC-Sample
Contribute to flutterwtf/Flutter-gRPC-Sample development by creating an account on GitHub.

There we can find gRPC implementation with a Dart server and a Flutter client.

It`s a simple app where user can create his account, write posts and leave comments to posts.

Let’s have a look at it.

Preparation Steps:

  1. In our case the first thing we have to do is to check whether we have Dart and Flutter successfully installed on our computer. To do that, enter the following command in your terminal/cmd: flutter --version;
  2. The next step is installing the protobuf compiler mentioned above. Here are instructions on how to do that;
  3. One more step before actually starting writing code is to install protoc_plugin for Dart/Flutter (obviously this is only for Dart/Flutter-developers).

So now we are ready.

Writing Proto

First let’s create our .proto files where we will define our services.

The first .proto file we are going to create will be called general.proto

syntax = "proto3"; 

message Empty {}

enum ProtoAction { 
  CREATE = 0;
  DELETE = 1;
}

As you can see here we only define some data types.

The first one named Empty is defined with message keyword and has nothing inside. We will talk about it a bit later. The ProtoAction is a simple enum.


Now let`s create another file called post.proto :

syntax = "proto3";

import "google/protobuf/timestamp.proto";
import "general.proto";

service PostService {
  rpc CreatePost(ProtoPost) returns (Empty);
  rpc GetAllPosts(Empty) returns (stream ProtoPost);
  rpc DeletePost (ProtoPostId) returns (Empty);
}

message ProtoPostId {
  int64 id = 1;
}

message ProtoPost {
  int64 id = 1;
  int64 user_id = 2;
  google.protobuf.Timestamp date = 3;
  string text = 4;
  string user_name = 5;
  ProtoAction action = 6;
}

We use message keyword to define our own data types. Each message type has fields and each field has a number to uniquely identify it in the message type. In this file we create only two data types, ProtoPostId and ProtoPost.

We use service keyword to define our services. In order to call a method, the method must be referenced by its service. This is analogous to class and methods. In this file we create only one service, PostsService.

We use rpc keyword to define methods inside services. In this file we have 3 methods inside our service, they are:

  • CreatePost which takes ProtoPost object as a parameter and returns Empty object (which we defined in general.proto);
  • GetAllPosts which takes Empty object as a parameter and returns a stream of ProtoPost objects (server-side streaming);
  • DeletePost which takes ProtoPostId object as a parameter and returns Empty object (which we defined in general.proto).
💡
Other .proto files you can find in the project.

Compilation

After this we need to compile our .proto files. Run the below command to compile the general.proto file:

protoc -I=. --dart_out=grpc:. post.proto

The I=. tells the compiler the source folder which proto file we are trying to compile.

The dart_out=grpc:. subcommand tells the protoc compiler that we’re generating Dart source code from the post.proto definitions and using it for gRPC =grpc:. The . tells the compiler to write the dart files in the root folder we are operating from.

💡
You should run this command for each .proto file you want to compile.

This command will generate the following files:

  • post.pb.dart;
  • post.pbenum.dart;
  • post.pbgrpc.dart;
  • post.pbjson.dart.

One of the most important files is post.pb.dart, which contains Dart source code for the message data structures in the post.proto file.

Another important one is post.pbgrpc.dart which contains the class PostServiceClient that we’ll use to create instances to call the rpc methods and an interface PostServiceBase. This interface will be implemented by the server to add the methods’ implementations.

Dart Coding

The next step is creating our dart server (implementing interfaces). Here is a code of post_service.dart :

class PostService extends PostServiceBase {
  final DatabaseDataSource _databaseDataSource = DatabaseDataSource();
  final CommentService _commentService = CommentService();
  final StreamController<ProtoPost> _postsStream = StreamController.broadcast();
  
  @override
  Future<Empty> createPost(ServiceCall call, ProtoPost request) async {
    final post = _databaseDataSource.createPost(request);
    post.freeze();
    var createdPost = post.rebuild((post) => post.action = ProtoAction.CREATE);
    _postsStream.add(createdPost);
    return Empty();
  }
  
  @override
  Future<Empty> deletePost(ServiceCall call, ProtoPostId request) async {
    var post = _databaseDataSource.getPost(request.id);
    post.freeze();
    var deletedPost = post.rebuild((post) => post.action = ProtoAction.DELETE);
    _postsStream.sink.add(deletedPost);
    
    for (var comment in _databaseDataSource.getCommentsByPostId(request.id)) {
      await _commentService.deleteComment(call, ProtoCommentId(id: comment.id));
    }
    _databaseDataSource.deletePost(request.id);
    return Empty();
  }
  
  @override
  Stream<ProtoPost> getAllPosts(ServiceCall call, Empty request) async* {
    for (var post in _databaseDataSource.getAllPosts()) {
      yield post;
    }
    await for (var post in _postsStream.stream) {
      yield post;
    }
  }
}

As you can see we have implemented our PostServiceBase interface and have overrode all the methods that were defined in our post.proto file.

💡
You`ve probably noticed that in this file we have several imports of other proto-generated files and other services. The full source code is available on the GitHub repository. The link is above.

And finally let`s see how we can call our server methods on our client. This is a code of remote_data_provider.dart file:

class RemoteDataProvider {
  UserServiceClient? _userServiceClient;
  PostServiceClient? _postServiceClient;
  CommentServiceClient? _commentServiceClient;
  late final ClientChannel _channel;
  
  RemoteDataProvider() {
    _createChannel();
  }
  
  UserServiceClient get userServiceClient {
    if (_userServiceClient != null) return _userServiceClient!;
    _userServiceClient = UserServiceClient(_channel);
    return _userServiceClient!;
  }
  
  PostServiceClient get postServiceClient {
    if (_postServiceClient != null) return _postServiceClient!;
    _postServiceClient = PostServiceClient(_channel);
    return _postServiceClient!;
  }
  
  CommentServiceClient get commentServiceClient {
    if (_commentServiceClient != null) return _commentServiceClient!;
    _commentServiceClient = CommentServiceClient(_channel);
    return _commentServiceClient!;
  }
  
  void _createChannel() {
    _channel = ClientChannel(
      host,
      port: port,
      options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
    );
  }
  
  void dispose() async {
    await _channel.shutdown();
  }
}

As I already mentioned above, in order to call our rpc methods from server we need to create [Name]ServiceClient instances. The important moment here is that we also need to create ClientChannel instance, which we will later pass to each of our [Name]ServiceClient objects. And then we just use them (below are some examples of PostServiceBase usage):

@override
Future<void> createPost(Post post) async {
 await _postServiceClient.createPost(_postMapper.toProto(post));
}

@override
Future<void> deletePost(int postId) async {
  await _postServiceClient.deletePost(_postMapper.toProtoPostId(postId));
}

When to Use

We already know a lot about gRPC. Let`s think about real cases of use:

  • Microservices connection: due to the high performance of gRPC, this technology will be a great solution for connecting architectures that consist of lightweight microservices;
  • Multi-language systems: with its built-in code generation, gRPC will be very useful when managing connections within a polyglot environment;
  • Real-time streaming: since gRPC is built on HTTP 2, it makes it the most convenient technology for such cases when real-time communication is a requirement;
  • Low-power low-bandwidth networks: gRPC’s use of serialized Protobuf messages offers light-weight messaging, greater efficiency, and speed for bandwidth-constrained, low-power networks (especially when compared to JSON).
Share this post