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:

Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
— https://developers.google.com/protocol-buffers

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:

  1. Install the latest version of ProtoC from their releases page here

  2. Add the bin folder to your path (Note you can use apt-get and brew to easily install in non-windows environments)

  3. Once it is installed, add the proto dart plugin:

    • dart pub global activate protocplugin

    • flutter dart pub global activate protocplugin

  4. 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.

image_2021-10-06_140210.png

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.

  1. Create a new folder inside of lib called proto

  2. Open a terminal in your IDE of choice

  3. 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

  • Language Guide for Proto3

  • 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!

5pid8s.jpg

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

Previous
Previous

Managing Flutter Versions with Flutter Version Manager

Next
Next

Featured Flutter Package - Spider