Using built_value

Much more than just a solution for automating serialization logic, built_value (along with its partner package, built_collection ) is designed to help you create classes that function as value types. For this reason, instances of classes created with built_value are immutable. You can create new instances (including copies of existing ones), but once they’re built, their properties can’t be changed.

To accomplish this, built_value uses the same source generation approach found in json_serializable , but creates a lot more code. In the generated file for a built_value class, you’ll find:

An equality ( == ) operator

) operator A hashCode property

property A toString method

method A serializer class, if you want one — more on that below

A “builder” class used to make new instances

This adds up to several hundred lines, even for a small class like SimpleObject, so I’ll avoid showing it here. The actual class file (what you’d write as a dev) looks like this:

The differences between this and the version of SimpleObject we started with are:

A part file is declared, just like json_serializable .

. An interface, Built<SimpleObject, SimpleObjectBuilder> , is being implemented.

, is being implemented. A static getter for a serializer object has been added.

Nullability annotations are on all the fields. These are optional, but to make this example match the others in functionality, I’ve added them.

Two constructors (one private, one factory) have been added, and the original one was removed.

SimpleObject is now an abstract class!

The differences between this and the version of SimpleObject we started with are:

Let’s start with the last point: SimpleObject has become an abstract class. In its generated file, built_value defines a subclass of SimpleObject called _$SimpleObject , and that’s where it provides a lot of the new functionality. It’s where you’ll find the new hashCode property, new methods relating to immutability, and so on. Each time you create an instance of SimpleObject , you’re actually getting _$SimpleObject under the hood. You’ll never need to reference it by the derived type, though, so your app code still uses SimpleObject to declare and use references.

This is possible because instantiation of a brand-new SimpleObject is done through a generated factory constructor, which you can see referenced in the last line of the file above. To use it, you pass in a method that sets properties on a SimpleObjectBuilder (it’s the “b” parameter below), which builds the immutable object instance for you:

You can also rebuild to get a modified bopy of an existing instance:

As you can see, the constructor in SimpleObject has been made private through the use of an underscore:

That guarantees that your app’s code can’t directly instantiate an instance of SimpleObject . In order to get one, you have to use the factory constructor, which uses SimpleObjectBuilder and always produces an instance of the _$SimpleObject subclass.

That’s great, but I thought we were talking about deserialization?

I’m getting to that! To serialize and deserialize instances, you’ll need to add a little code somewhere in the app (creating a file called serializers.dart , for example, is a good approach):

This file does two things. First, it uses the @SerializersFor annotation to instruct built_value to create serializers for a list of classes. In this case, there’s only one class, so it’s a short list. Second, it creates a global variable called serializers that references the Serializers object that handles serialization of built_value classes. It’s used like this:

As with json_serializable , transforming an object into and out of JSON is still mostly a one line affair, with the generated code doing the heavy lifting for you. One important thing to note, though, is this bit from serializers.dart :

built_value is designed to be as extensible as possible, and it includes a plugin system for defining custom serialization formats (you could, for example, write one to translate to and from XML or your own binary format). I’m using it in this example to add a plugin called StandardJsonPlugin because, by default, built_value doesn’t use the map-based JSON format that you’re probably used to.

Instead, it uses a list-based format. For example, a simple object with String , int , and double members would be serialized like this:

Rather than this:

There are a few reasons why built_value prefers the list-based format, which for sake of space I’ll leave to the package documentation. For this example, though, just know that you can easily make use of map-based JSON serialization through the StandardJsonPlugin , which ships as part of the built_value package.

Conclusion

So those are the high points of all three techniques. As I mentioned back at the beginning of this article, choosing the right one is mostly about considering the scope of your project, how many people are working on it, and what other needs you have for your model objects.

The next step is to start coding, so come see us at the Flutter Dev Google Group, StackOverflow, or The Boring Show, and let us know how it goes!