Boost Your Flutter Apps with gRPC: A Step-by-Step Guide
If you need only the code you can check this repo 👇
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:
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:
- 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
; - The next step is installing the protobuf compiler mentioned above. Here are instructions on how to do that;
- 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 takesProtoPost
object as a parameter and returnsEmpty
object (which we defined in general.proto);GetAllPosts
which takesEmpty
object as a parameter and returns a stream ofProtoPost
objects (server-side streaming);DeletePost
which takesProtoPostId
object as a parameter and returnsEmpty
object (which we defined in general.proto).
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.
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.
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).