本文由 简悦SimpRead 转码,原文地址 www.raywenderlich.com
本章将教你如何将数据从JSON字符串序列化到Dart模型类。这就是ne......
在本章中,你将学习如何将JSON数据序列化为模型类。一个模型类代表一个特定对象的数据。一个例子是菜谱模型类,它通常有一个标题、一个配料表和烹饪步骤。
你将继续之前的项目,也就是本章的入门项目,你将添加一个对食谱和其属性进行建模的类。然后你将把这个类集成到现有的项目中。
在本章结束时,你将知道。
- 如何将JSON序列化到模型类中。
- 如何使用Dart工具从JSON中自动生成模型类。
什么是JSON?
JSON是JavaScript Object Notation的缩写,是一种开放标准的格式,在网络和移动客户端中使用。它是服务器提供的基于Representational State Transfer(REST)的API中最广泛使用的格式(en.wikipedia.org/wiki/Repres…)。如果你与一个拥有REST API的服务器交谈,它很可能会以JSON格式返回数据。一个JSON响应的例子是这样的。
{
"recipe": {
"uri": "http://www.edamam.com/ontologies/edamam.owl#recipe_b79327d05b8e5b838ad6cfd9576b30b6",
"label": "Chicken Vesuvio"
}
}
这是一个配方响应的例子,它包含配方对象中的两个字段。
虽然可以将JSON视为一个长字符串,并尝试解析出数据,但使用一个已经知道如何做的包要容易得多。Flutter有一个内置的包用于解码JSON,但在本章中,你将使用json_serializable和json_annotation包来帮助使这个过程更容易。
Flutter内置的dart:convert包包含json.decode和json.encode等方法,可以将JSON字符串转换为Map<String, dynamic>并返回。虽然这比手动解析JSON领先一步,但你仍然需要编写额外的代码,将该地图的值放到一个新的类中。
json_serializable包很方便,因为它可以根据你通过json_annotation提供的注释为你生成模型类。在看看自动序列化之前,你将在下一节看到手动序列化所需要的东西。
自己写代码
那么你如何自己去写代码来序列化JSON呢?典型的模型类有toJson()和fromJson()方法,所以你将从这些方法开始。
要将上面的JSON转换为模型类,你首先要创建一个Recipe模型类。
class Recipe {
final String uri;
final String label;
Recipe({this.uri, this.label});
}
你不需要在你的项目中输入这个,因为在下一节中你将转为自动序列化。
然后你要添加toJson()和fromJson()工厂方法。
factory Recipe.fromJson(Map<String, dynamic> json) {
return Recipe(json['uri'] as String, json['label'] as String);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{ 'uri': uri, 'label': label}
}
在fromJson()中,你从名为json的JSON地图变量中抓取数据,并将其转换为参数,传递给Recipe构造函数。在toJson()中,你使用JSON字段名构建一个地图。
虽然手工为两个字段做这些并不费力,但如果你有多个模型类,每个都有,比如,五个字段,或者更多呢?如果你重命名其中一个字段呢?你会记得重命名那个字段的所有出现的地方吗?
你拥有的模型类越多,维护它们背后的代码就越复杂。不要担心,这就是自动化代码生成的救星。
自动生成JSON序列化
在本章中你将使用两个包:json_annotation和来自Google的json_serializable。
你使用第一个包来给模型类添加注释,这样json_serializable就可以生成辅助类,将JSON从字符串转换为模型,然后再转换回来。
要做到这一点,你用@JsonSerializable()注解标记一个类,这样构建器包就可以为你生成代码。类中的每个字段应该与JSON字符串中的字段名称相同,或者使用@JsonKey()注解给它一个不同的名称。
大多数构建器包通过导入所谓的 .part 文件来工作。这将是一个为你生成的文件。你所需要做的就是创建一些工厂方法,这些方法将调用生成的代码。
添加必要的依赖项
打开projects文件夹中的starter项目。在Flutter dependencies部分下面的pubspec.yaml添加以下包,并与shared_preferences对齐。^2.0.5`:
json_annotation: ^3.1.1
在 "dev_dependencies "部分,在 "flutter_test "部分之后,添加。
build_runner: ^1.10.0
json_serializable: ^3.5.1
确保这些都是正确缩进的。结果应该是这样的。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
cached_network_image: ^2.5.0
flutter_slidable: ^0.5.7
flutter_svg: ^0.19.3
shared_preferences: ^2.0.5
json_annotation: ^3.1.1
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^1.10.0
json_serializable: ^3.5.1
build_runner是一个所有代码生成器都需要的包,以便构建**.part**文件类。
最后,按下你应该在文件顶部看到的Pub get按钮。现在你已经准备好生成模型类了。
从JSON生成类
你试图序列化的JSON看起来像。
{
"q": "pasta",
"from": 0,
"to": 10,
"more": true,
"count": 33060,
"hits": [
{
"recipe": {
"uri": "http://www.edamam.com/ontologies/edamam.owl#recipe_09b4dbdf0c7244c462a4d2622d88958e",
"label": "Pasta Frittata Recipe",
"image": "https://www.edamam.com/web-img/5a5/5a5220b7a65c911a1480502ed0532b5c.jpg",
"source": "Food Republic",
"url": "http://www.foodrepublic.com/2012/01/21/pasta-frittata-recipe",
}
]
}
q字段是查询。在这个例子中,你要查询的是意大利面。from是起始索引,to是结束索引。more是一个布尔值,告诉你是否有更多的项目要检索,而count是你可能收到的项目总数。hits数组是实际的菜谱列表。
在本章中,你将使用配方项目的label和image字段。你的下一步是生成为这些数据建模的类。
创建模型类
首先,在lib文件夹下创建一个名为network的新目录。在这个文件夹中,创建一个名为recipe_model.dart的新文件。然后添加需要的导入。
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'recipe_model.g.dart';
json_annotation库让你把类标记为可序列化。文件recipe_model.g.dart还不存在,你将在后面的步骤中生成它。
接下来,添加一个名为APIRecipeQuery的类,并加上@JsonSerializable()注释。
@JsonSerializable()
class APIRecipeQuery {
}
这标志着APIRecipeQuery类是可序列化的,所以json_serializable包可以生成**.g.dart**文件。
通过使用Command-Click打开JsonSerializable的定义,你会看到你可以为这个类改变一些设置。
final bool nullable;
/// Creates a new [JsonSerializable] instance.
const JsonSerializable({
this.anyMap,
this.checked,
this.createFactory,
this.createToJson,
this.disallowUnrecognizedKeys,
this.explicitToJson,
this.fieldRename,
this.ignoreUnannotated,
this.includeIfNull,
this.nullable,
this.genericArgumentFactories,
});
例如, 你可以让这个类nullable并添加额外的检查来正确验证JSON。
转换为JSON或从JSON转换
现在,回到recipe_model.dart,在APIRecipeQuery类中添加这些方法用于JSON转换。
factory APIRecipeQuery.fromJson(Map<String, dynamic> json) => _$APIRecipeQueryFromJson(json);
Map<String, dynamic> toJson() => _$APIRecipeQueryToJson(this);
注意,箭头运算符右边的方法还不存在。你将在以后通过运行build_runner命令来创建它们。
还请注意,第一个调用是一个工厂方法。这是因为你在创建实例时需要一个类级别的方法,而你在已经存在的对象上使用另一个方法。
现在,在方法后面添加以下字段。
@JsonKey(name: 'q')
String query;
int from;
int to;
bool more;
int count;
List<APIHits> hits;
@JsonKey注解指出,你在JSON中用字符串q表示query字段。其余的字段在JSON中看起来就像它们的名字一样。你将在下面定义APIHits。
接下来,添加这个构造函数。
APIRecipeQuery({
@required this.query,
@required this.from,
@required this.to,
@required this.more,
@required this.count,
@required this.hits,
});
@required注解说这些字段在创建一个新的实例时是必须的。
然后,在同一文件的底部,添加一个名为 "APIHits "的新类,定义如下。
// 1
@JsonSerializable()
class APIHits {
// 2
APIRecipe recipe;
// 3
APIHits({
@required this.recipe,
});
// 4
factory APIHits.fromJson(Map<String, dynamic> json) =>
_$APIHitsFromJson(json);
Map<String, dynamic> toJson() => _$APIHitsToJson(this);
}
下面是这段代码的作用。
- 标志着该类可序列化。
- 定义一个`APIRecipe'类的字段,你将很快创建这个字段。
- 定义了一个接受
recipe参数的构造函数。 - 添加JSON序列化的方法。
接下来添加APIRecipe类的定义。
@JsonSerializable()
class APIRecipe {
// 1
String label;
String image;
String url;
// 2
List<APIIngredients> ingredients;
double calories;
double totalWeight;
double totalTime;
APIRecipe({
@required this.label,
@required this.image,
@required this.url,
@required this.ingredients,
@required this.calories,
@required this.totalWeight,
@required this.totalTime,
});
// 3
factory APIRecipe.fromJson(Map<String, dynamic> json) =>
_$APIRecipeFromJson(json);
Map<String, dynamic> toJson() => _$APIRecipeToJson(this);
}
// 4
String getCalories(double calories) {
if (calories == null) {
return '0 KCAL';
}
return calories.floor().toString() + ' KCAL';
}
// 5
String getWeight(double weight) {
if (weight == null) {
return '0g';
}
return weight.floor().toString() + 'g';
}
在这里,你
- 定义菜谱的字段。
label是显示的文本,image是要显示的图片的URL。 - 说明每个食谱都有一个成分列表。
- 创建用于序列化JSON的工厂方法。
- 添加一个辅助方法,把卡路里变成一个字符串。
- 添加另一个辅助方法,将重量转化为字符串。
最后,添加APIIngredients。
@JsonSerializable()
class APIIngredients {
// 1
@JsonKey(name: 'text')
String name;
double weight;
APIIngredients({
@required this.name,
@required this.weight,
});
// 2
factory APIIngredients.fromJson(Map<String, dynamic> json) =>
_$APIIngredientsFromJson(json);
Map<String, dynamic> toJson() => _$APIIngredientsToJson(this);
}
在这里,你
- 说明这个类的
name字段映射到名为text的JSON字段。 - 创建方法来序列化JSON。
对于你的下一步,你将创建**.part**文件。
生成.part文件
通过点击左下角的面板,或者选择视图▸工具窗口▸终端,在Android Studio中打开终端,然后输入。
flutter pub run build_runner build
预期的输出将看起来像这样。
[INFO] Generating build script...
...
[INFO] Creating build script snapshot......
...
[INFO] Running build...
...
[INFO] Succeeded after ...
注意:如果你在运行该命令时遇到问题,请确保你已经在电脑上安装了Flutter,并且设置了一个路径指向它。
该命令在network文件夹中创建recipe_model.g.dart。如果你没有看到这个文件,右击网络文件夹,选择从磁盘重新加载。
注意:如果你仍然没有看到它,请重新启动Android Studio,这样它在启动时就能识别新生成的文件的存在。
如果你想让程序在你每次对文件进行修改时运行,你可以使用watch命令,像这样。
flutter pub run build_runner watch
该命令将继续运行并观察文件的变化。现在,打开recipe_model.g.dart。这里是第一个生成的方法。
// 1
APIRecipeQuery _$APIRecipeQueryFromJson(Map<String, dynamic> json) {
return APIRecipeQuery(
// 2
query: json['q'] as String,
// 3
from: json['from'] as int,
to: json['to'] as int,
more: json['more'] as bool,
count: json['count'] as int,
// 4
hits: (json['hits'] as List)
?.map((e) =>
e == null ? null : APIHits.fromJson(e as Map<String, dynamic>))
?.toList(),
);
}
注意,它需要一个String到dynamic的映射,这是Flutter中典型的JSON数据。键是字符串,值将是一个基元、一个列表或另一个映射。该方法。
- 返回一个新的
APIRecipeQuery类。 - 将
q'键映射到query'字段。 - 将
from整数映射到from字段,并映射其他字段。 - 将
hits列表的每个元素映射到APIHits类的一个实例。
你可以自己写这段代码,但它会变得有点繁琐,而且容易出错。让一个工具为你生成代码可以节省大量的时间和精力。查看文件的其余部分,看看生成的代码是如何将JSON数据转换为所有其他模型类的。
热重启应用程序,确保它仍然像以前一样编译和工作。你不会在用户界面上看到任何变化,但代码现在已经被设置为解析配方数据。
测试生成的JSON代码
现在你已经具备了从JSON中解析模型对象的能力,你将读取启动项目中包含的一个JSON文件,并展示一张卡片,以确保你可以使用生成的代码。
打开ui/recipes/recipe_list.dart,在顶部添加以下导入。
import 'dart:convert';
import '../../network/recipe_model.dart';
import 'package:flutter/services.dart';
import '../recipe_card.dart';
在_RecipeListState中的List<String> previousSearches = <String>[];之后,添加。
APIRecipeQuery _currentRecipes1;
然后,在`initState()'之后,添加。
Future loadRecipes() async {
// 1
final jsonString = await rootBundle.loadString('assets/recipes1.json');
setState(() {
// 2
_currentRecipes1 = APIRecipeQuery.fromJson(jsonDecode(jsonString));
});
}
这个方法。
- 从assets目录中加载recipes1.json。
rootBundle是顶层属性,持有对资产文件夹中所有项目的引用。这个方法以字符串的形式加载文件。 - 使用内置的
jsonDecode()方法将字符串转换为地图,然后使用为你生成的fromJson()来制作一个APIRecipeQuery实例。
接下来,在initState()中调用loadRecipes()。
@override
void initState() {
super.initState();
loadRecipes();
// ... rest of method
}
在文件的顶部添加以下导入。
import 'recipe_details.dart';
这将使我们能够使用配方的详细信息页面。在该类的底部添加。
Widget _buildRecipeCard(BuildContext context, List<APIHits> hits,
int index) {
// 1
final recipe = hits[index].recipe;
return GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return const RecipeDetails();
},
));
},
// 2
child: recipeStringCard(recipe.image, recipe.label),
);
}
这个方法。
- 在给定的索引处找到配方。
- 调用
recipeStringCard(),在搜索栏下面显示一个漂亮的卡片。
现在,将现有的_buildRecipeLoader()替换为以下内容。
Widget _buildRecipeLoader(BuildContext context) {
// 1
if (_currentRecipes1 == null || _currentRecipes1.hits == null) {
return Container();
}
// Show a loading indicator while waiting for the recipes
return Center(
// 2
child: _buildRecipeCard(context, _currentRecipes1.hits, 0),
);
}
现在这段代码。
- 检查查询或菜谱列表是否为 "空"。
- 如果不是,则使用列表中的第一个项目调用
_buildRecipeCard()。
执行热重启,应用程序将显示一个鸡肉维苏威样本卡。
现在,数据模型类按预期工作,你已经准备好从网上加载食谱。系好你的安全带。 :]
关键点
- JSON是一种开放的标准格式,在网络和移动客户端中使用,特别是与REST APIs。
- 在移动应用程序中,JSON代码通常被解析为你的应用程序将使用的模型对象。
- 你可以自己写JSON解析代码,但通常让JSON包为你生成解析代码会更容易。 json_annotation和json_serializable是可以让你生成解析代码的包。
从这里开始,该往哪里走?
在本章中,你已经学会了如何创建可以从JSON中解析的模型,然后在你从网络中获取JSON数据时使用。如果你想了解更多关于json_serializable的信息,请到pub.dev/packages/js…。
在下一章中,你将在目前所做的基础上,学习从互联网上获取数据。