Building Models in Dart using Google Protocol Buffers
Hey all! This should be a good one.
So in Dart there are a ton of packages to build PODOs (Plain old dart objects) for your data models to be used for a number of things, such as database entry and just transformations for views to name a few. A load of these I can vouch for and have used in the past (Freezed for example).
So the question goes, where to from here? And why might you want to take a look at Protocol Buffers over some of these well established options.
So a brief introduction is needed, to quote Google:
The in short is, Google use these models for a number of things already. They allow them to take data in a number of different programming languages, and serialize them in a reusable way. A core example of this is Firestore, where they use Protocol Buffers to serialize your data and fire them over gRPC in order to store them in your database. The bidirectional capabilities of gRPC allow them to easily listen for updates without needing to write the client controllers on their end.
Getting Started
Before we do anything, it is important to note that PB is installed via a number of different executables. There is always one which will parse your .proto files (protoc) and then a number of ones based on the programming languages you want to export the models into.
All of these models can be set immutable to match capabilities of other packages; but a load of other features are included, for example deep cloning, merging, and serialization construction and deconstruction just to name a few.
Follow these steps to get installed:
Install the latest version of ProtoC from their releases page here
Add the
bin
folder to your path (Note you can use apt-get and brew to easily install in non-windows environments)Once it is installed, add the proto dart plugin:
dart pub global activate protocplugin
flutter dart pub global activate protocplugin
Finally add the protobuf Dart package to prevent errors in your built files:
flutter pub add protobuf
dart pub add protobuf
You should then be able to see the installed executable by running protoc —version
in your terminal of choice.
Creating Your First Model
All protocol buffers are built from a template file known as a .proto file. They have a look and feel like a class structure with a few notable differences.
Create a new folder in the root of your project and call it proto
, once here create your first model called hello.proto
.
Note: You may wish to install a plugin to provide syntax highlighting. For example vscode-proto3 in vscode.
Once inside of this file. Add the following code:
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
string email = 3;
}
A few things to point out here. Firstly notice the syntax line at the top. This indicates the version of PB to use.
Inside of this file you will have Messages and Services. Messages are your models, and will be generated into Dart classes for you to use. Services we are not using here but can be used to generate the controller for your services. For example in node backends if you wish to use gRPC.
The numbers are used to reference the order of data. Unlike JSON for example, the serialized output is raw; meaning that the data is obfuscated by default and is smaller. This is so that it can be passed down a socket instead of a REST API easily, and so that the data in transit is as small as possible.
Building Your Model
Now just follow these steps to build your first model.
Create a new folder inside of lib called
proto
Open a terminal in your IDE of choice
Run
protoc --dart_out=grpc:lib -Ipb proto/*.proto --proto_path .
from within your terminal
This should output correctly and make a few files inside of your proto folder.
hello.pb.dart (The models fields)
hello.pbenum.dart (The models enums if included)
hello.pbjson.dart (Helpers for serializing to JSON)
Using Your Model
Once done, it is as simple as creating any new class. Just make sure to call .Create as the factory to make sure the model is mutable.
@override
Widget build(BuildContext context) {
final Person personModel = Person.create()
..name = 'Ryan'
..email = 'ryan.dixon@inqvine.com'
..age = 27;
return Scaffold(
body: Center(
child: Text(personModel.name),
),
);
}
Useful Tips
Call model.toProto3Json() to serialize to JSON (For example to store in Firestore)
Use Model.create()..mergeFromProto3Json(firestoreSnapshot.data(), ignoreUnknownFields: true) to deserialize Firestore data while ignoring unknown fields
Use model.mergeFromMessage to merge two PB files together
Use the optional keyword and model.hasValue to implement an equivalent to null checking for your models.
All models will have default values when created, even if the optional keyword is given. They are null safe by default!
Make use of different protoc plugins for your language of choice! I use them namely for Typescript, Dart, and Go to communicate across clients
There is an experimental plugin to built Firestore rules from the PB files. I have not tried this yet, but it looks good!
Google provides a list of “Well known” types. You might recognize a few of these from Firestore!
When To Use
One word of note before choosing this as your serialization tool of choice. Protocol buffers are a serialization tool, nothing else. There are other solutions so if you’re purely just trying to create models and so if you have no intention of using these between systems in some form of data layer, then it is MASSIVELY overkill. That being said, it is lovely for when you do want to optimise absolutely everything.
Migrate into these if your data layer is limiting you. Don’t just slap these in your app and be done with it!
Conclusion
Protocol buffers are more verbose than some of the other code generation solutions. The benefit is in the built in extensions, the support from Google, and the ability to easily pass these between languages. If you’re struggling with JSON data, and manually creating models when your domain layer changes. I highly recommend giving this a go!
Trust me, I’ve only just touched on the start of their power. They can be incredibly powerful if used properly and completely replace REST in your architecture if used on top of gRPC as mentioned. Stay tuned next time to see how we can integrate this into a backend service built in dart.
See you next time!
Ryan