Serialization to JSON and an immutable object. About the built_value package for Flutter





Sometimes JSON from an API needs to be converted to an object and preferably to an immutable value. On Dart it is possible, but it requires a lot of coding for each of the objects. Fortunately, there is a package that will help you do all this, and in this article I will tell you about this method.



Our goal:



1. Serialization



final user = User.fromJson({"name": "Maks"});
final json = user.toJson();


2. Use as values



final user1 = User.fromJson({"name": "Maks"});
final user2 = User((b) => b..name='Maks');
if (user1 == user2) print('    ');


3. Immutability



user.name = 'Alex'; // 
final newUser = user.rebuild((b) => b..name='Alex'); // 




Install packages



Open the pubspec.yaml file in our Flutter project and add the built_value package to dependencies :



  ...
  built_value: ^7.1.0


And also add the built_value_generator and build_runner packages to dev_dependencies . These packages will help you generate the required codes.



dev_dependencies :



 ...
  build_runner: ^1.10.2
  built_value_generator: ^7.1.0


Save the pubspec.yaml file and run “ flutter pub get ” to get all the required packages.



Create built_value



Let's create a simple class to see how this works.



Create a new file user.dart :



import 'package:built_value/built_value.dart';

part 'user.g.dart';

abstract class User implements Built<User, UserBuilder> {
  String get name;

  User._();
  factory User([void Function(UserBuilder) updates]) = _$User;
}


So, we created a simple abstract User class with one name field , indicated that our class is part of user.g.dart and the main implementation is there, including UserBuilder . To automatically create this file, you need to run this on the command line:



flutter packages pub run build_runner watch


or



flutter packages pub run build_runner build


We start with watch so as not to restart every time something changes in the class.



After that, we see that a new user.g.dart file has appeared . You can see what's inside and see how much time we will save by automating this process. When we add more fields and serialization, this file will become even larger.



Let's check what we got:



final user = User((b) => b..name = "Max");
print(user);
print(user == User((b) => b..name = "Max")); // true
print(user == User((b) => b..name = "Alex")); // false


nullable



Add a new surname field to the User class:



abstract class User implements Built<User, UserBuilder> {
  String get name;
  String get surname;
...
}


If you try like this:



final user = User((b) => b..name = 'Max');


Then we get an error:



Tried to construct class "User" with null field "surname".


To make surname optional, usenullable:



@nullable
String get surname;


or you need to give surname every time :



final user = User((b) => b
  ..name = 'Max'
  ..surname = 'Madov');
print(user);


Built Collection



Let's use arrays. BuiltList will help us for this :



import 'package:built_collection/built_collection.dart';
...
abstract class User implements Built<User, UserBuilder> {
  ...
  @nullable
  BuiltList<String> get rights;
...


final user = User((b) => b
  ..name = 'Max'
  ..rights.addAll(['read', 'write']));
print(user);


Enum



It is necessary to restrict rights so that it does not take any other values than ' read ', ' write ' and ' delete '. To do this, create a new file named right.dart create a new EnumClass :



import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';

part 'right.g.dart';

class Right extends EnumClass {
  static const Right read = _$read;
  static const Right write = _$write;
  static const Right delete = _$delete;

  const Right._(String name) : super(name);

  static BuiltSet<Right> get values => _$rightValues;
  static Right valueOf(String name) => _$rightValueOf(name);
}


User:



@nullable
BuiltList<Right> get rights;


Now rights only accept the Right type :



final user = User((b) => b
  ..name = 'Max'
  ..rights.addAll([Right.read, Right.write]));
print(user);


Serialization



So that these objects can be easily converted to JSON and back, we need to add a couple more methods to our classes:



...
import 'package:built_value/serializer.dart';
import 'serializers.dart';
...
abstract class User implements Built<User, UserBuilder> {
...
  Map<String, dynamic> toJson() => serializers.serializeWith(User.serializer, this);
  static User fromJson(Map<String, dynamic> json) =>
serializers.deserializeWith(User.serializer, json);

  static Serializer<User> get serializer => _$userSerializer;
}


In principle, this is enough for serialization:



static Serializer<User> get serializer => _$userSerializer;


But for convenience, let's add the toJson and fromJson methods .



We also add one line to the Right class:



import 'package:built_value/serializer.dart';
,,,
class Right extends EnumClass {
...
  static Serializer<Right> get serializer => _$rightSerializer;
}


And you need to create another file called serializers.dart :



import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:built_value_demo/right.dart';
import 'package:built_value_demo/user.dart';

part 'serializers.g.dart';

@SerializersFor([Right, User])
final Serializers serializers =
(_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();


Each new Built class needs to be added to @SerializersFor ([ ... ]) for serialization to work as expected .



Now we can check what we got:



final user = User.fromJson({
  "name": "Max",
  "rights": ["read", "write"]
});
print(user);
print(user.toJson());


final user2 = User((b) => b
  ..name = 'Max'
  ..rights.addAll([Right.read, Right.write]));
print(user == user2); // true


Let's change the values:



final user3 = user.rebuild((b) => b
  ..surname = "Madov"
  ..rights.replace([Right.read]));
print(user3);


Additionally



As a result, there will be those who will say that it is still necessary to write quite a lot. But if you use Visual Studio Code, I recommend installing a snippet called Built Value Snippets and then you can generate all this automatically. To do this, search the Marketplace or follow this link .



After installation write “ bvin the Dart file and you can see what options exist.



If you don't want Visual Studio Code to show the generated “ * .g.dart ” files, you need to open Settings and look for Files: Exclude , then click on Add Pattern and add “** / *. g.dart ”.



What's next?



At first glance, it may seem that so much effort is not worth it, but if you have a lot of such classes, this will greatly facilitate and speed up the whole process.



PS I would be very glad and grateful to you if you would share your methods, which you find more practical and more effective than the one proposed by me.



GitHub project



Packages:

pub.dev/packages/built_value

pub.dev/packages/built_value_generator

pub.dev/packages/build_runner



All Articles