The text/Andrew Brogdon

Source: Google Developer account

At some point, most applications need to interact with the outside world and get data from online terminal addresses. With Dart’s HTTP package, making an HTTPS GET request to get a weather forecast or final World Cup score is simple:

1    import 'dart:async';    

2    import 'package:http/http.dart' as http; 

3

4    final response = await http.get(myEndpointUrl);    

5    if (response.statusCode == 200) {    

6    // use the data in response.body    

7    } else {    

8    // handle a failed request    

9    } 
Copy the code

The data in Response.body could be a JSON string, and we still have some work to do before we can use it for widgets. First, you need to parse the string into a more manageable JSON representation. You must then convert that representation to a model or some other strongly typed variable so that you can use the strings efficiently.

Fortunately, the Dart team and community have had a lot of discussion about JSON and are able to provide a solution. I will introduce three solutions in order of lowest complexity, namely, handwritten constructors, JSON_serializable, and built_value.

The call to deserialize the data using all three methods is very similar. The lines for the handwritten constructor and jSON_serialIZABLE look like this:

1    final myObject = SimpleObject.fromJson(json.decode(aJsonString));
Copy the code

The built_value deserialization call looks like this:

1    final myObject = serializers.deserializeWith(  

2        SimpleObject.serializer, json.decode(aJsonString)); 
Copy the code

The real difference is how much code is generated for you in the “SimpleObject” class, and what that code does.

Handwritten constructor

The least complex approach: don’t generate the code for you.

You can do whatever you want, but you have to maintain it.

json_serializable

Generate the fromJson constructor and toJson method for you.

Before building the application, you need to add several packages to the project and generate some files using source_gen.

Customizing the generated resources can be tricky.

built_value

Generate code for serialization, immutability, toString methods, hashCode attributes, and so on. This is a heavyweight solution with many features.

As with JSON_serialIZABLE, you need to import many packages and use source_gen.

Extensible serialization architecture based on plug-ins.

Insight into instance creation and variability.

As discussed below, which content library is appropriate for you depends on the details of the project, particularly the project size and state management approach. Handwritten constructors are helpful for projects of interest that have a maintainer, while applications built by large distributed teams that need immutable models to keep logic clear can really benefit from “built_value.”

For now, however, let’s start at the beginning: parsing JSON from a string into a more user-friendly in-memory representation. Whatever approach you decide to take later, this step in the relevant process is the same.

Parsing JSON

You can convert JSON strings to an intermediate format using the DART: Convert library:


1    import 'dart:convert'; 

2

3    try {

4        final parsed = json.decode(aJsonStr);

5    } on FormatException catch (e) { 

6        print("That string didn't look like Json.");

7    } on NoSuchMethodError catch (e) { 

8        print('That string was null! '); 9}Copy the code

If the String contains valid JSON, the system returns a dynamic reference to List or Map<String, dynamic>, depending on whether the JSON String has an array or a single object. We’re almost done with simple things like integer lists. Before using it, you might want to create a second strongly typed data reference:

1    final dynamicListOfInts = json.decode(aJsonArrayOfInts);

2

3    // Create a strongly typed list with references to the data that are casted

4    // immediately. This is probably the better approach for data model classes.

5    final strongListOfInts = List<int>.from(dynamicListOfInts); 

6

7    // Or create a strongly typed list with references that will be lazy-casted 

8    // when used.

9    final anotherStrongListOfInts = List<int>.from(dynamicListOfInts);

Copy the code

It’s the more complex payloads that are interesting. Converting Map<String, Dynamic > to an actual model object may involve converting defaults, NULL, and nested objects. If you later decide to rename or add/remove attributes, many things can go wrong and many troublesome details need to be updated.

Handwritten constructor

We have to start somewhere, right? If you have a small application and the data is not very complex, it helps to write your own factory constructor that takes the Map<String, dynamic> parameter. For example, if you want to get the following data:

Note: Factory constructor links

www.dartlang.org/guides/lang…

1 {2"aString": "Blah, blah, blah.", 

3        "anInt": 1,

4        "aDouble": 1.0,

5        "aListOfStrings": ["one"."two"."three"6],"aListOfInts": [1, 2, 3], 

7        "aListOfDoubles": [1.0, 2.0, 3.0] 8}Copy the code

The code for the matching class might look like this:

1    class SimpleObject {

2        const SimpleObject({

3            this.aString,

4            this.anInt,

5            this.aDouble,

6            this.aListOfStrings,

7            this.aListOfInts,

8            this.aListOfDoubles,

9        });

10

11        final String aString;

12        final int anInt;

13        final double aDouble; 

14        final List<String> aListOfStrings;

15        final List<int> aListOfInts; 

16        final List<double> aListOfDoubles;

17

18        factory SimpleObject.fromJson(Map<String, dynamic> json) {

19            if (json == null) {

20                throw FormatException("Null JSON provided to SimpleObject"); 21} 22 23return SimpleObject(

24                    aString: json['aString'],

25                    anInt: json['anInt'], 

26                    aDouble: json['aDouble'],

27                    aListOfStrings: json['aListOfStrings'] != null

28                            ? List<String>.from(json['aListOfStrings'])

29                            : null,

30                    aListOfInts: json['aListOfInts'] != null

31                            ? List<int>.from(json['aListOfInts'])

32                            : null, 

33                    aListOfDoubles: json['aListOfDoubles'] != null

34                            ? List<double>.from(json['aListOfDoubles']) 35 : null, 36 ); 37}} 38Copy the code

Then use the named fromJson factory constructor as follows:

1    return SimpleObject(

2        aString: json['aString']????"",

3        anInt: json['anInt']???? 0, 4 aDouble: json['aDouble']???? 1.0, 5 aListOfStrings: a List < String >. The from (json ['aListOfStrings']???? []), 6 aListOfInts: List<int>.from(json['aListOfInts']???? []), 7 aListOfDoubles: List<double>.from(json['aListOfDoubles'] ?? []), 

8    );
Copy the code

The downside is that you need to hand write about 20 lines of constructor code, which you now have to maintain. As your application grows and the number of data classes starts to grow into dozens, you might be thinking, “Gee, coding these JSON constructors is getting boring, if only you could automatically generate code based on the properties of your data classes.”

As it turns out, you can do just that with the JSON_serializable library.

Using json_serializable

Before we get to JSON_serializable, we need to change gears and briefly discuss another package.

Flutter does not currently support mapping, so some of the techniques available in other contexts (such as the Android JSON library’s ability to check annotation classes at runtime) are not available to Flutter developers. However, they can use the Dart package named source_gen. This package provides utilities and a basic framework to automatically generate source code.

Source_gen does not update your code directly, but creates a separate new file next to it. By convention, there is a “G” in the file name, so if your data class exists in Model.dart, source_gen creates Model.g.art. You can use the part keyword to reference the corresponding file in the original location, and the compiler will embed the file.

The JSON_serializable package uses the Source_gen API to generate serialization code and writes the fromJson constructor (along with the toJson method) for you.

Note: JSON_serialIZABLE link

Pub.dartlang.org/packages/js…

The basic process for using it in an application is as follows:

Import the JSON_SERIalIZABLE and JSON_ANNOTATION packages into your project.

Define the data classes as usual.

Add the @JSonSerializable annotation to the class definition.

Add something else to associate this data class with the JSON_serialIZABLE code created for it.

Run source_gen to generate the code.

Note: Import your project link

Github.com/dart-lang/j…

I’m going to go through each of these steps.

Import the JSON_serialIZABLE package into your project

You can find jSON_serializable in the Dart package directory. Simply update your Pubspec.yaml as instructed.

Note: Dart package directory link

Pub.dartlang.org/packages/js…

Update your pubspec.yaml link

Flutter. IO/using – packa…

Defining data classes

There’s nothing special about this part. Build a data class using base properties and constructors. The properties you plan to serialize should be value types or other classes used with jSON_serialIZABLE.

1 class SimpleObject { 2 SimpleObject({ 3 this.aString, 4 this.anInt, 5 this.aDouble, 6 this.aListOfStrings, 7 this.aListOfInts, 8 this.aListOfDoubles, 9 }); 10 11 final String aString; 12 final int anInt; 13 final double aDouble; 14 final List<String> aListOfStrings; 15 final List<int> aListOfInts; 16 final List<double> aListOfDoubles; 17}Copy the code

Add @jsonSerializable annotation

The jSON_serialIZABLE package only generates code for data classes that have been marked with the @jSONSerialIZABLE annotation:

1    import 'package:json_annotation/json_annotation.dart'; 2 3 @JsonSerializable 4 class SimpleObject { 5 ... 6}Copy the code

Associate the generated code with your code

Next are the three changes that associate the class definition with its corresponding part file:

1    import 'package:json_annotation/json_annotation.dart'; 

2

3    part 'simple_object.g.dart';

4

5    @JsonSerializable()

6    class SimpleObject extends Object with _$SimpleObjectSerializerMixin {

7        SimpleObject({ 

8            this.aString, 

9            this.anInt,

10            this.aDouble,

11            this.aListOfStrings,

12            this.aListOfInts,

13            this.aListOfDoubles,

14        });

15

16        final String aString; 

17        final int anInt;

18        final double aDouble;

19        final List<String> aListOfStrings;

20        final List<int> aListOfInts;

21        final List<double> aListOfDoubles;

22

23        factory SimpleObject.fromJson(Map<String, dynamic> json) => 

24                _$SimpleObjectFromJson(json); 25}Copy the code

The first of these is the part declaration, which tells the compiler to embed simple_object.g.art (more on that later). Then update the data class definition to use mixins. Finally, update the data class to use the fromJson constructor. The last two changes each reference code in the generated file.

Run source_gen

Use the following command to trigger code generation from your project folder:

flutter packages pub run build_runner build
Copy the code

When you’re done, you’ll have a new file named Simple_object.g.Art next to the original file. The file content is as follows:

1    part of 'simple_object.dart';

2

3    SimpleObject _$SimpleObjectFromJson(    

4                    Map<String, dynamic> json) =>    

5            new SimpleObject(    

6                    aString: json['aString'] as String,    

7                    anInt: json['anInt'] as int,    

8                    aDouble: (json['aDouble'] as num)? .toDouble(), 9 aListOfStrings: 10 (json['aListOfStrings'] as List)? .map((e) => e as String)? .toList(), 11 aListOfInts: 12 (json['aListOfInts'] as List)? .map((e) => e as int)? .toList(), 13 aListOfDoubles: (json['aListOfDoubles'] as List) 14 ? .map((e) => (e as num)? .toDouble()) 15 ? .toList()); 16 17 abstract class _$SimpleObjectSerializerMixin { 

18        String get aString;    

19        int get anInt;    

20        double get aDouble;    

21        List<String> get aListOfStrings;    

22        List<int> get aListOfInts;    

23        List<double> get aListOfDoubles;    

24        Map<String, dynamic> toJson() => <String,dynamic>{    

25                    'aString': aString,    

26                    'anInt': anInt,    

27                    'aDouble': aDouble,    

28                    'aListOfStrings': aListOfStrings,    

29                    'aListOfInts': aListOfInts,    

30                    'aListOfDoubles': aListOfDoubles 31 }; 32}Copy the code

The first method is called using the fromJson constructor in The SimpleObject, and the Mixin class provides a new toJson method for the SimpleObject. Both are easy to use:

1    final myObject = SimpleObject.fromJson(json.decode(jsonString));    

2    final myJsonStr = json.encode(myObject.toJson()); 
Copy the code

From a quantitative point of view, by adding three lines of code to simple_object.dart, you can write 20 fewer lines of constructor code than if you had used the other method. You can also regenerate code whenever you want to rename or adjust properties. In addition, you can get the toJson method that we provide for free. That’s not bad.

But what if you want to serialize to multiple formats? Or is it more than JSON? What if you need something else, such as immutable model objects? For these use cases, built_value comes in handy.

Using built_value

Built_value (and its partner package built_Collection) is much more than an automatic serialization logic solution, designed to help you create data classes that act as value types. To do this, data class instances created using built_value are immutable. You can create new instances (including copies of existing ones), but you cannot change the properties of an instance once it has been built.

To do this, built_value uses the same source generation method found in jSON_serializable, but creates more code. In the generated file for the built_Value class, you will find:

An equality (==) operator

A hashCode property

A toString method

A serializer class (if you want one), more on that below

A “Builder” class for creating new instances

Even small classes like SimpleObject add up to hundreds of lines, so I won’t go over them here. The actual class file (the one you wrote as a developer) looks like this:


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

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

3    import 'package:built_value/serializer.dart';

4

5    part 'simple_object.g.dart';

6

7    abstract class SimpleObject

8            implements Built<SimpleObject, SimpleObjectBuilder> { 

9        static Serializer<SimpleObject> get serializer => 

10                _$SimpleObjectSerializer; 

11

12        @nullable

13        String get aString;

14

15        @nullable

16        int get anInt;

17

18        @nullable

19        double get aDouble; 

20

21        @nullable

22        BuiltList<String> get aListOfStrings;

23

24        @nullable

25        BuiltList<int> get aListOfInts; 

26

27        @nullable

28        BuiltList<double> get aListOfDoubles;

29

30        SimpleObject._();

31

32        factory SimpleObject([updates(SimpleObjectBuilder b)]) =

33                _$SimpleObject; 34}Copy the code

The difference between this file and the SimpleObject version we started with is that:

Declare the part file as jSON_serialIZABLE.

Built<SimpleObject, SimpleObjectBuilder>

Added static getters for serializer objects.

All fields are annotated as Null or not. These annotations are optional, but I’ve added them to make this example match other related examples.

Add two constructors (one private and one factory) and remove the original function.

SimpleObject is now abstract!

The difference between this file and the SimpleObject version we started with is that:

Let’s start with the last point: The SimpleObject has become an abstract class. In the generated file, built_value defines a name named _SimpleObject. But you never need to refer to it as a derived type, so your application code will still use The SimpleObject to declare and use reference content.

This is possible because you have already instantiated the brand new SimpleObject through the generated factory constructor. You can see a reference to it in the last line of the file above. To get started, you need to pass in a method that sets the properties on SimpleObjectBuilder (the “b” argument below) and builds immutable object instances for you:

1 final SimpleObject myObject = SimpleObject((b) => b 2 .. aString ='Blah, blah, blah'3.. anInt = 1 4 .. ADouble = 1.0 5.. aListOfStrings = ListBuilder<String>(['one'.'two'.'three'6]).. aListOfInts = ListBuilder<int>([1, 2, 3]) 7 .. AListOfDoubles = ListBuilder< Double >([1.0, 2.0, 3.0]) 8);Copy the code

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

1 final SimpleObject anotherObject = myObject.rebuild((b) => b 2 .. aString ="An updated value"3);Copy the code

You can see that the constructors in the SimpleObject have become closed constructors by using underscores:

1    SimpleObject._();
Copy the code

This ensures that your application code does not instantiate the SimpleObject instance directly. To get the instance, you must use the factory constructor, which uses SimpleObjectBuilder and always produces instances of the _$SimpleObject subclass.

That’s nice, but we’re talking about deserialization.

This is it! To serialize and deserialize instances, you’ll need to add some code somewhere in your application (for example, creating a file called serializers.dart is a good idea) :

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

2    import 'package:built_value/serializer.dart';

3    import 'package:built_value/standard_json_plugin.dart';

4    import 'simple_object.dart'; 

5

6    part 'serializers.g.dart';

7

8    @SerializersFor(const [

9    SimpleObject,

10    ])

11

12    final Serializers serializers =

13        (_$serializers.toBuilder().. addPlugin(StandardJsonPlugin())).build();Copy the code

This document does two things. First, it uses the @serializersfor annotation to instruct Built_Value to create a serializer for the list of data classes. In this case, there is only one data class, so the list is short. Second, it creates a global variable called the serializer, which refers to the serializer object that handles the built_Value class serialization. The usage is as follows:

1    final myObject = serializers.deserializeWith(

2    SimpleObject.serializer, json.decode(aJsonString));

3

4    final String myJsonString = son.encode(serializers.serialize(myObject));
Copy the code

As with JSON_serializable, because the generated code can do the heavy lifting for you, converting an object to JSON or from JSON to other formats, it still only takes one line in most cases. One thing to note is this code from serializers. Dart:

1    final Serializers serializers =

2            (_$serializers.toBuilder().. addPlugin(StandardJsonPlugin())).build();Copy the code

Built_value is designed to be as extensible as possible, and includes a plug-in system for defining custom serialization formats (for example, you could write one to convert to XML and from XML or your own binary format). In this example, I used it to add a plug-in called StandardJsonPlugin, because by default, built_value does not use the map-based JSON format that you might normally use.

Instead, it uses a list-based format. For example, a simple object with String, int, and double components would be serialized as follows:

1/2"SimpleObject",    

3        "aString"4,"Blah, blah, blah"May,"anInt", June 1, 7"aDouble", 8 2.0 9]Copy the code

Instead of this:

1 {2"$": "SimpleObject",    

3        "aString": "Blah, blah, blah"4,"anInt": 1,    

5        "aDouble": 2.0 6}Copy the code

There are several reasons why Built_Value prefers to use a list-based form. Since space is limited, I will explain this in the package documentation. For this example, you only need to know that you can easily use map-based JSON serialization with the StandardJsonPlugin, which comes with the Built_Value package.

Note: Package documentation links

Github.com/google/buil…

conclusion

Those are the highlights of all three techniques. As I mentioned at the beginning of this article, choosing the right technology is primarily about the scope of your project, the number of people working on it, and your other needs for model objects.

The next step is to start coding, head over to Flutter Dev Google Group, StackOverflow, or The Boring Showl and let us know how you get on!

Note: Flutter Dev Google Group link

groups.google.com/forum/#! The for…

StackOverflow link

Stackoverflow.com/questions/t…

The Boring Showl link

www.youtube.com/watch?v=TiC…