[Flutter学徒] 10 - 用JSON进行序列化

3,577 阅读11分钟

本文由 简悦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_serializablejson_annotation包来帮助使这个过程更容易。

Flutter内置的dart:convert包包含json.decodejson.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数组是实际的菜谱列表。

在本章中,你将使用配方项目的labelimage字段。你的下一步是生成为这些数据建模的类。

创建模型类

首先,在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);
}


下面是这段代码的作用。

  1. 标志着该类可序列化。
  2. 定义一个`APIRecipe'类的字段,你将很快创建这个字段。
  3. 定义了一个接受recipe参数的构造函数。
  4. 添加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';
}


在这里,你

  1. 定义菜谱的字段。label是显示的文本,image是要显示的图片的URL。
  2. 说明每个食谱都有一个成分列表。
  3. 创建用于序列化JSON的工厂方法。
  4. 添加一个辅助方法,把卡路里变成一个字符串。
  5. 添加另一个辅助方法,将重量转化为字符串。

最后,添加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);
}


在这里,你

  1. 说明这个类的name字段映射到名为text的JSON字段。
  2. 创建方法来序列化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(),
  );
}


注意,它需要一个Stringdynamic的映射,这是Flutter中典型的JSON数据。键是字符串,值将是一个基元、一个列表或另一个映射。该方法。

  1. 返回一个新的APIRecipeQuery类。
  2. q'键映射到query'字段。
  3. from整数映射到from字段,并映射其他字段。
  4. 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));
  });
}


这个方法。

  1. assets目录中加载recipes1.jsonrootBundle是顶层属性,持有对资产文件夹中所有项目的引用。这个方法以字符串的形式加载文件。
  2. 使用内置的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),
  );
}


这个方法。

  1. 在给定的索引处找到配方。
  2. 调用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),
  );
}


现在这段代码。

  1. 检查查询或菜谱列表是否为 "空"。
  2. 如果不是,则使用列表中的第一个项目调用_buildRecipeCard()

执行热重启,应用程序将显示一个鸡肉维苏威样本卡。

现在,数据模型类按预期工作,你已经准备好从网上加载食谱。系好你的安全带。 :]

关键点

  • JSON是一种开放的标准格式,在网络和移动客户端中使用,特别是与REST APIs。
  • 在移动应用程序中,JSON代码通常被解析为你的应用程序将使用的模型对象。
  • 你可以自己写JSON解析代码,但通常让JSON包为你生成解析代码会更容易。 json_annotationjson_serializable是可以让你生成解析代码的包。

从这里开始,该往哪里走?

在本章中,你已经学会了如何创建可以从JSON中解析的模型,然后在你从网络中获取JSON数据时使用。如果你想了解更多关于json_serializable的信息,请到pub.dev/packages/js…

在下一章中,你将在目前所做的基础上,学习从互联网上获取数据。


www.deepl.com 翻译