Json data parsing has always been a sore point in Flutter development, especially for developers moving from iOS, Android or Java, where they are used to parsing Json data into object entities and then using them. This is a more cumbersome step to take with Flutter.

Flutter is developed using the Dart language, which has no reflection, so reflection cannot be used to map Json data directly to the corresponding object entity class objects as Java does. The official solution is to convert the Json data to a dictionary, and then use it to fetch numbers from the dictionary. But fetching numbers directly from the dictionary is inconvenient, writing code without automatic prompts is unfriendly, and you can miswrite field names while writing.

Based on the present situation of Flutter, convenient development calls, can be manually again after converting the Json dictionary mapping entity to the object in the field, such use can directly use the corresponding entity class object, but this method will lead to the development process to write a lot of redundant code, because each class is to write the corresponding mapping code manually, Seriously affect the development efficiency. As a result, there are many auto-generated schemes for mapping Json to object entity class code, Plug-ins such as Json2Dart, JsonToDart, JsonToDart Class, FlutterJsonBeanFactory, and third-party libraries such as JSON_TO_Model. The essence of this is to automate the generation of mapping code that requires developers to write manually.

After continuous attempts and experiments, the author found that there were more or less some defects in these schemes. After constantly weighing and comparing and combining with the actual use of the development, the author finally chose to use the FlutterJsonBeanFactory plug-in plus some custom code modifications, and finally achieved the effect of rapid use in the project.

The rest of this article will focus on how to quickly implement Json parsing using the FlutterJsonBeanFactory plug-in in conjunction with custom code modifications.

0. Install the plug-in

Install FlutterJsonBeanFactory in the Android Studio plugin market.

Remember to restart Android Studio after installation, otherwise you may not be able to generate code. File->Invalidate Caches/Restart Let’s reboot this way.

After reboot, right-click New in the project directory to see a menu description of JsonToDartBeanAction.

1. Create an entity class

1.1 create

Click New => JsonToDartBeanAction on the directory and the create Model Class interface will pop up as shown below:

  • Class Name: The Name of the Class to be created
  • JSON Text: Sample data corresponding to JSON for the class
  • Null-able: indicates whether it is null safe. If this parameter is not selected, all generated fields are of non-null type. If this parameter is selected, all generated fields are of nullable type

Fill in the name of the Class to be created and the Json sample data for the corresponding Class, and click Make to generate the corresponding Class code.

Entitiy suffix is added to the generated entity class and corresponding file name by default. If you do not need or need to change to another suffix, you can set it in plug-in Settings:

The generated directory structure is as follows:

  • Models is the project’s own directory, that is, right-click the directory to create the entity class, the generated entity class is stored in the directory;

  • Generated /json is the generated directory for the plug-in, where xxx_entity.g art is the auxiliary method generated based on the entity class, and the base directory is the basic public code

A detailed parsing of each generated file is provided below.

1.2 xxx_entity. Dart

Dart generates the xxx_entity.dart file in the target directory, which contains the code for the entity class. Dart generates user_entity.dart and corresponds to the entity class UserEntity as follows:

@JsonSerializable(a)class UserEntity {
   String? id;
   String? name;
   int? age;

  UserEntity();

  factory UserEntity.fromJson(Map<String.dynamic> json) => $UserEntityFromJson(json);

  Map<String.dynamic> toJson() => $UserEntityToJson(this);

  @override
  String toString() {
    return jsonEncode(this); }}Copy the code

The plugin automatically generates a field corresponding to the entity class. If null-able is selected, the field type is nullable. .

In addition to the fields, a factory method for fromJson is generated, as well as a toJson method for converting fromJson to an entity class and from an entity class toJson. The corresponding methods called are $XxxEntityFromJson and $XxxEntityToJson. The corresponding methods are implemented in the.g.art file

Finally, we rewrite the toString method to convert entities to Json strings.

The generated entity class is annotated with @JSonSerialIZABLE (), and the annotated entity class will be found when the code is regenerated later.

After the entity class is generated, if you want to modify the entity field, such as adding fields or modifying the field type and name, you can use Alt + J to regenerate the corresponding code after modification.

1.3 xxx_entity. G.d art

Xxx_entity. g art is the auxiliary method file corresponding to the entity class and is stored in the generated/json directory with the suffix. $XxxFromJson ($XxxFromJson); $XxxToJson ($XxxFromJson);

UserEntity $UserEntityFromJson(Map<String.dynamic> json) {
    final UserEntity userEntity = UserEntity();
    final String? id = jsonConvert.convert<String>(json['id']);
    if(id ! =null) {
            userEntity.id = id;
    }
    final String? name = jsonConvert.convert<String>(json['name']);
    if(name ! =null) {
            userEntity.name = name;
    }
    final int? age = jsonConvert.convert<int>(json['age']);
    if(age ! =null) {
            userEntity.age = age;
    }
    return userEntity;
}

Map<String.dynamic> $UserEntityToJson(UserEntity entity) {
    final Map<String.dynamic> data = <String.dynamic> {}; data['id'] = entity.id;
    data['name'] = entity.name;
    data['age'] = entity.age;
    return data;
}
Copy the code
  • $XxxFromJsonExtract the corresponding fields of the Json data and assign them to the corresponding fields of the entity class. Json data is converted into entity fields usedjsonConvert.convertThe definition injson_convert_content.dartIn the.
  • $XxxToJsonTransform entity data into a Map dictionary.

1.4 json_convert_content. Dart

Dart jSON_convert_content. dart json_convert_content.dart json_convert_content.dart json_convert_content.dart json_convert_content.dart json_convert_content.dart json_convert_content.

JsonConvert jsonConvert = JsonConvert();

class JsonConvert {

    T? convert<T>(dynamicvalue) {... }List<T? >? convertList<T>(List<dynamic>? value) {... }List<T>? convertListNotNull<T>(dynamicvalue) {... } T? asT<Textends Object?> (dynamicvalue) {... }static M? _fromJsonSingle<M>(Map<String.dynamic> json) {... }static M? _getListChildType<M>(List<dynamic> data) {... }static M? fromJsonAsT<M>(dynamic json) {...}
} 
Copy the code

A global jsonConvert variable is created at the beginning of the file for direct invocation elsewhere.

Here is a detailed description of what each JsonConvert method does:

convert

Convert convert Json data to entity objects, source code as follows:

T? convert<T>(dynamic value) {
  if (value == null) {
    return null;
  }
  return asT<T>(value);
}
Copy the code

The code is very simple. It first checks whether the incoming data is NULL. If null is returned, the asT method is called. Non-list fields in the generated.g. art $UserEntityFromJson method are basically converted by calling the convert method.

convertList

ConvertList converts Json data into a List of entity objects.

List<T? >? convertList<T>(List<dynamic>? value) {
  if (value == null) {
    return null;
  }
  try {
    return value.map((dynamic e) => asT<T>(e)).toList();
  } catch (e, stackTrace) {
    print('asT<$T> $e $stackTrace');
    return<T>[]; }}Copy the code

The code is also very simple. First, it determines whether the incoming data is NULL. If it is null, it returns NULL directly. A try-catch is added to the conversion to return an empty List if an error is reported.

convertListNotNull

ConvertListNotNull converts Json data to a List of entities. ConvertListNotNull converts Json data to a List of entities.

List<T>? convertListNotNull<T>(dynamic value) {
  if (value == null) {
    return null;
  }
  try {
    return (value as List<dynamic>).map((dynamice) => asT<T>(e)!) .toList(); }catch (e, stackTrace) {
    print('asT<$T> $e $stackTrace');
    return<T>[]; }}Copy the code

The difference between convertList and convertList is that the argument is passed List

whereas convertListNotNull is passed directly dynamic. The second biggest difference is that convertListNotNull is added to the asT when the asT method is called! Is not empty.

When a field is defined as a List type in an entity class, the choice is to generate convertList or convertListNotNull to convert it, depending on whether it is a non-empty type:

  • List<Xxxx? >?: used when a List is defined as a nullable type and the elements in the List are also nullable typesconvertList
  • List<Xxxx>?: used when a List is defined as a nullable type, but the elements in the List are of a non-nullable typeconvertListNotNull
  • List<Xxxx>?: used when a List is defined as a non-empty type and the elements in the List are of non-empty typeconvertListNotNull

asT

Convert, convertList, convertListNotNull all call asT methods, source code is as follows:

T? asT<T extends Object?> (dynamic value) {
  if (value is T) {
    return value;
  }
  final String type = T.toString();
  try {
    final String valueS = value.toString();
    if (type == "String") {
      return valueS as T;
    } else if (type == "int") {
      final int? intValue = int.tryParse(valueS);
      if (intValue == null) {
        return double.tryParse(valueS)? .toInt()asT? ; }else {
        return intValue asT; }}else if (type == "double") {
      return double.parse(valueS) as T;
    } else if (type ==  "DateTime") {
      return DateTime.parse(valueS) as T;
    } else if (type ==  "bool") {
      if (valueS == '0' || valueS == '1') {
        return (valueS == '1') as T;
      }
      return (valueS == 'true') as T;
    } else {
      returnJsonConvert.fromJsonAsT<T>(value); }}catch (e, stackTrace) {
    print('asT<$T> $e $stackTrace');
    return null; }}Copy the code

The asT method is a bit more code than the above three methods, but it’s actually quite simple.

First determine whether the data type passed in is the data type to be converted, and if so, return the passed parameter directly. That is, if you want to convert the passed data to User, but the passed parameter itself is of type User, then return it directly.

Then, t.tostring () is used to get the name of the generic type and compare it with the basic data types String, int, double, DateTime, bool, etc. If these types are used, the conversion method of these types is called to convert them.

Finally, the fromJsonAsT method is called if it is not an underlying type.

fromJsonAsT

static M? fromJsonAsT<M>(dynamic json) {
  if(json == null) {return null;
  }
  if (json is List) {
    return _getListChildType<M>(json);
  } else {
    return _fromJsonSingle<M>(json as Map<String.dynamic>); }}Copy the code

Check whether the incoming Json data is null. If null is returned, null is returned. Then determine if the Json data is a List or not, call _getListChildType or _fromJsonSingle.

_fromJsonSingle

_fromJsonSingle for a single entity object conversion, source:

static M? _fromJsonSingle<M>(Map<String.dynamic> json) {
  final String type = M.toString();
  if(type == (UserEntity).toString()){
    return UserEntity.fromJson(json) as M;
  }

  print("$type not found");

  return null;
}
Copy the code

The type name of the generic type is first obtained through the m.Tostring () method, and then compared to the generated entity type, invoking the fromJson method of the corresponding entity class. For example, UserEntity here, to determine that the generic type name is equal to userentity.toString (), call userentity.fromjson. If multiple entity classes are created through the plug-in, there will be multiple similar if statements.

_getListChildType

_getListChildType = getListChildType;

static M? _getListChildType<M>(List<dynamic> data) {
  if(<UserEntity>[] is M){
    return data.map<UserEntity>((e) => UserEntity.fromJson(e)).toList() as M;
  }

  print("${M.toString()} not found");

  return null;
}
Copy the code

Unlike _fromJsonSingle, instead of using generic type names, an empty List of the corresponding entity class is created to determine whether it is a generic type, as shown above

[] is M. If the type is the same, the map calls the fromJson method of the corresponding entity class for the conversion. Similarly, if you create multiple entity classes, there will be multiple similar if statements.

So you end up calling the entity class’s fromJson method, which instead calls the $UserEntityFromJson method generated in xxxx_entity.g.art.

The overall process is as follows:

1.5 json_field. Dart

Includes JsonSerializable and JSONField annotations. The directory is generated/json/base

class JsonSerializable{
    const JsonSerializable();
}

class JSONField {
  //Specify the parse field name
  final String? name;

  //Whether to participate in toJson
  final bool? serialize;
  
  //Whether to participate in fromMap
  final bool? deserialize;

  const JSONField({this.name, this.serialize, this.deserialize});
}
Copy the code
  • JsonSerializable class annotation, which is generated when the plug-in looks for the annotation class when the code is second generated.
  • JSONField field annotations for customizing field mappings and configuring whether to serialize and deserialize fields

2. Use

2.1 Single entity Resolution

Json data can be parsed into entity objects by directly calling the fromJson method corresponding to the entity class.

String userData = """ { "id":"12313", "name":"loongwind", "age":18 } """;
UserEntity user = UserEntity.fromJson(jsonDecode(userData));
Copy the code

The fromJson argument is a Map, so you need to convert the Json string to a Map using jsonDecode

In addition to using the entity class’s fromJson method directly, you can also parse it directly using the generated JsonConvert:

String userData = """ { "id":"12313", "name":"loongwind", "age":18 } """;

UserEntity? user = jsonConvert.convert<UserEntity>(jsonDecode(userData));

UserEntity? user = jsonConvert.asT<UserEntity>(jsonDecode(userData));

UserEntity? user = JsonConvert.fromJsonAsT<UserEntity>(jsonDecode(userData));
Copy the code

The same parsing effect can be achieved using JsonConvert’s Convert, asT, and fromJsonAsT methods.

2.2 the List parsing

ConvertList, convertListNotNull, convertList, convertListNotNull, convertList, convertListNotNull, convertList, convertListNotNull, convertList, convertListNotNull, convertList, convertListNotNull, convertList, convertListNotNull

String userData = """ [ { "id":"12313", "name":"loongwind", "age":18 }, { "id":"22222", "name":"cmad", "age":25 } ] """;

List<UserEntity>? users = jsonConvert.convert<List<UserEntity>>(jsonDecode(userData));

List<UserEntity>? users = jsonConvert.asT<List<UserEntity>>(jsonDecode(userData));

List<UserEntity>? users = JsonConvert.fromJsonAsT<List<UserEntity>>(jsonDecode(userData));

List<UserEntity? >? users = jsonConvert.convertList<UserEntity>(jsonDecode(userData));List<UserEntity>? users = jsonConvert.convertListNotNull<UserEntity>(jsonDecode(userData));
Copy the code

ConvertList and convertListNotNull differ from convert, asT, and fromJsonAsT in that the former is the generic type of the List Item element, while the latter is the type of the corresponding List directly. For example, convertList and convertListNotNull generics are directly UserEntity, while convert, asT, and fromJsonAsT generics are List

.

2.3 Use of JSONField

Custom field name

JSONField can be used to implement custom field mapping when Json data fields are inconsistent with the fields in the code. For example, the field name in Json does not conform to the code specification.

For example, if the Json field is AGE and needs to be mapped to the AGE field of the entity class, just add JSONField annotation to the AGE field of the entity class, specify name as AGE, and then use Alt + J to generate the code again:

   String? id;
   String? name;
   @JSONField(name: "AGE")
   int? age;
Copy the code

After adding @jsonField annotations, you must use Alt + J to regenerate the code, otherwise it will not take effect

String userData = """ { "id":"12313", "name":"loongwind", "AGE":18 } """;

UserEntity user = UserEntity.fromJson(jsonDecode(userData));
print(user.age) / / 18
Copy the code

This allows the AGE to be mapped to the AGE field.

Ignore the field

JSONField also has two fields serialize and deserialize, which are used to ignore a field when serializing or deserializing. For example, if the name field is not parsed, set deserialize to false. If toJson does not serialize a field, set serialize to false.

@JSONField(deserialize: false)
String? name;
//------
String userData = """ { "id":"12313", "name":"loongwind", "AGE":18 } """;

UserEntity user = UserEntity.fromJson(jsonDecode(userData));
print(user.name); // null


@JSONField(serialize: false)
String? name;
//------
UserEntity user = UserEntity();
user.id = "123";
user.name = "loongwind";
user.age = 18;

print(user.toJson()); // {id: 123, AGE: 18}
Copy the code

If @jsonfield (deserialize: false) is set to null, the Json data will not be parsed. Similarly, if @jsonfield (deserialize: false) is set to null. False), when toJson is called, it does not have a field even if it has a value converted toJson data.

3. The optimization

The basic usage of Json data parsing after entity classes are generated using plug-ins has been explained above, but there are some problems in actual project development. In actual project development, the data format returned by the interface generally looks like this:

{
  "code": 200."message": "success"."data": {"id": "12312312"."name": "loongwind"."age": 18}}Copy the code

In addition to the returned data, the data in the data field is the data needed by the actual business, and the data data structure returned by different interfaces is also different. If the plug-in is directly used, the following code will be generated:

@JsonSerializable(a)class UserResponseEntity {

	int? code;
	String? message;
	UserResponseData? data;
  
  UserResponseEntity();
  / /...
}

@JsonSerializable(a)class UserResponseData {

	String? id;
	String? name;
	int? age;
  
  UserResponseData();
	/ /...
}
Copy the code

This will generate a ResponseEntity class for each interface, which is not convenient to use and is not convenient for unified encapsulation. So we need to modify ResponseEntity to support generic resolution.

First reuse the Json sample data above to generate an ApiResponseEntity, then change the data field type to Dynamic and use Alt + J to regenerate the code:

@JsonSerializable(a)class ApiResponseEntity {

	int? code;
	String? message;
	dynamic data;
  
  ApiResponseEntity();

  factory ApiResponseEntity.fromJson(Map<String.dynamic> json) => $ApiResponseEntityFromJson(json);

  Map<String.dynamic> toJson() => $ApiResponseEntityToJson(this);

  @override
  String toString() {
    return jsonEncode(this); }}Copy the code

Remove the @jsonSerializable () annotation and place the API_response_entity. dart and api_response_entity.g.art files in a separate folder

Regenerating code with Alt + J will be generated according to the @JSonSerializable () annotation because the ApiResponseEntity class needs to be modified to meet the needs of generic resolution. So remove the @JsonSerializable() annotation to prevent regenerating code from overwriting custom code. After @jsonSerializable () is removed, the generated/json file will be automatically deleted, so you need to copy it to another directory to prevent it from being deleted next time.

Finally add generics support to ApiResponseEntity and ApiResponseEntityFromJson. The revised content is as follows:

class ApiResponseEntity<T> {

	int? code;
	String? message;
	T? data;
  
  ApiResponseEntity();

  factory ApiResponseEntity.fromJson(Map<String.dynamic> json) => $ApiResponseEntityFromJson<T>(json);

  Map<String.dynamic> toJson() => $ApiResponseEntityToJson(this);

  @override
  String toString() {
    return jsonEncode(this);
  }
}

ApiResponseEntity<T> $ApiResponseEntityFromJson<T>(Map<String.dynamic> json) {
	final ApiResponseEntity<T> apiResponseEntity = ApiResponseEntity<T>();
	final int? code = jsonConvert.convert<int>(json['code']);
	if(code ! =null) {
		apiResponseEntity.code = code;
	}
	final String? message = jsonConvert.convert<String>(json['message']);
	if(message ! =null) {
		apiResponseEntity.message = message;
	}
	final T? data = jsonConvert.convert<T>(json['data']);
	if(data ! =null) {
		apiResponseEntity.data = data;
	}
	return apiResponseEntity;
}
Copy the code

Add the generic T to the ApiResponseEntity and change the data type to T? , give $ApiResponseEntityFromJson method to add generics, analytical data, the data can be used directly when jsonConvert. Convert < T > parsing.

The following is an example:

  • Single entity resolution
String userData = """ { "code": 200, "message": "success", "data":{ "id": "12312312", "name": "loongwind", "age": 18 } } """;

ApiResponseEntity<UserEntity> response = ApiResponseEntity.fromJson(jsonDecode(userData));
print(response.data? .name);// loongwind
Copy the code
  • The List parsing
String userData = """ { "code": 200, "message": "success", "data":[ { "id": "12312312", "name": "loongwind", "age": 18 },{ "id": "333333", "name": "cmad", "age": 25 } ] } """;

ApiResponseEntity<List<UserEntity>> response = ApiResponseEntity.fromJson(jsonDecode(userData));
print(response.data? .length);/ / 2
print(response.data? .first.name);// loongwind
Copy the code
  • Basic data type parsing
String jsonData = """ { "code": 200, "message": "success", "data": 18 } """;

ApiResponseEntity<int> response = ApiResponseEntity.fromJson(jsonDecode(jsonData));
print(response.data); / / 18


String jsonData = """ { "code": 200, "message": "success", "data": "123456" } """;

ApiResponseEntity<String> response = ApiResponseEntity.fromJson(jsonDecode(jsonData));
print(response.data); / / 123456

String jsonData = """ { "code": 200, "message": "success", "data": true } """;

ApiResponseEntity<bool> response = ApiResponseEntity.fromJson(jsonDecode(jsonData));
print(response.data); // true
Copy the code

After the above modification, the ApiResponseEntity is suitable for use in project development.

Source: flutter_app_core

FlutterJsonBeanFactory plugin source code: FlutterJsonBeanFactory

Review: @ seven color xiangyun Zhi Zunbao

The application framework of Flutter

  • GetX integration and Usage of Flutter application framework
  • Frame construction of Flutter application (2) Screen adaptation
  • (3) Json data analysis