炫意html5
最早CSS3和HTML5移动技术网站之一

Flutter(Dart) 解析嵌套 JSON 为 model 对象的问题解决

最近在些 Flutter 应用时,用到了 GraphQL 分页,返回来的数据大概长下面这个样子,是按 Github API v4 的数据格式来整的:

{"users":{
{
"edges": [
{
"node": {
"id": "1",
"name": "ZhangSan"
},
"cursor": "MA=="
},
{
"node": {
"id": "2",
"name": "Lisi"
},
"cursor": "MB=="
}
],
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "MA==",
"endCursor": "MB=="
}
}
}

上面其实就是获取用户列表,这个列表在 GraphQL 里面有个专用的词语,叫 Connection ,它是嵌套的 JSON 格式数据,其中 node 里面就是实际的数据,比如这里是一个 User 对象,包含用户Id和用户名称。

在 flutter / dart 里面要使用这个数据,就要使用 fromJson 转换成实际的对象,也就是 model。

方案1 专用 Connection(可行,但重复代码多)

假设只有一个 User 需要 Pagination 分页,那就比较简单,直接这样创建一个 User 专用的 UserConnection

import 'package:termi/models/user.dart';
class UserConnection {
UserConnection({
this.edges,
this.pageInfo,
});
List<Edge> edges;
PageInfo pageInfo;
factory UserConnection.fromJson(Map<String, dynamic> json) => UserConnection(
edges: List<Edge>.from(json["edges"].map((x) => Edge.fromJson(x))),
pageInfo: PageInfo.fromJson(json["pageInfo"]),
);
Map<String, dynamic> toJson() => {
"edges": List<dynamic>.from(edges.map((x) => x.toJson())),
"pageInfo": pageInfo.toJson(),
};
}
class Edge {
Edge({
this.node,
this.cursor,
});
User node;
String cursor;
factory Edge.fromJson(Map<String, dynamic> json) => Edge(
node: User.fromJson(json["node"]),
cursor: json["cursor"],
);
Map<String, dynamic> toJson() => {
"node": node.toJson(),
"cursor": cursor,
};
}
class PageInfo {
//...
}

这个方式 PageInfo 类可以公用,但是 ConnectionEdge 都必须每个类重新写一遍。

但是我们希望 Connection 是一个公共类,所有对象都能用,所以这是一个方案,但不是最佳的方案。

方案2 泛型(不可行)

如果改成泛型,那是不是就可以呢?如下:

class Connection<T> {
//...
factory UserConnection.fromJson(Map<String, dynamic> json) => UserConnection(
edges: List<Edge<T>>.from(json["edges"].map((x) => Edge.fromJson(x))),
//...
);
//...
}
class Edge<T> {
//...
T node;
//...
factory Edge.fromJson(Map<String, dynamic> json) => Edge(
node: T.fromJson(json["node"]),
//..
);
//...
}

这样是不可以的,因为泛型没有 fromJson 方法,编译报错。

方案3 接口方式(不可行)

有 Java 或其他类似语言开发经验可能会想到接口类。

先定义一个接口,在 Dart 用抽象类代替接口:

abstract class AbstractModel {
factory AbstractModel.fromJson(Map<String, dynamic> json);
Map<String, dynamic> toJson();
}

然后每个 model 对象实现这个抽象类,如 User

class User implements AbstractModel {
@override
factory User.fromJson(Map<String, dynamic> json) {
// TODO: implement toJson
}

@override
Map<String, dynamic> toJson() {
// TODO: implement toJson
}
}

Connection 里面,直接以接口类作为类型,泛型都不用:

//...
class Edge {
//...
AbstractModel node;
//...
factory Edge.fromJson(Map<String, dynamic> json) => Edge(
node: AbstractModel.fromJson(json["node"]),
//..
);
//...
}

可以看到,这里存在 2 个问题会产生问题:

  • 在抽象类中不能用 factory,它实际就是一个静态方法,不能放在抽象类中,所以这个抽象类就会报错。
  • 在 Edge 类中,我们用都是类都 fromJson,所以这里还是不能直接指定到实际类,比如 User 类的 fromJson,只能用 AbstractModel.fromJson,但是这个是抽象类,也不能调用。

所以,这个方案不可行。

方案4 传递函数方式(可行,推荐)

其实 Dart 的函数可以最为参数来传递, Edge 里面的 fromJson 和具体的 Model 类相关,那我们把函数传进来。

第1步,把 User 类的 fromJson 由 factory 改为静态方法:

static fromJson(Map<String, dynamic> json) => User(
id: json["id"],
name: json["name"],
);

第2步,ConnectionEdgefromJson 方法都多传递一个 nodeFromJson 参数,类型是 Function

class Connection {
//...
factory UserConnection.fromJson(Map<String, dynamic> json, Function nodeFromJson) => Connection(
edges: List<Edge>.from(json["edges"].map((x) => Edge.fromJson(x, nodeFromJson))),
//...
);
//...
}
class Edge {
//...
dynamic node;
//...
factory Edge.fromJson(Map<String, dynamic> json, , Function nodeFromJson) => Edge(
node: nodeFromJson(json["node"]),
//..
);
//...
}

第3步,使用

Connection connection = Connection.fromJson(data["users"], Question.fromJson);
bool hasNextPage = connection.pageInfo.hasNextPage;
List<User> users = connection.edges
.map<User>((edge) => edge.node)
.toList();

这样即能确保不会编写重复代码,用起来也很清晰明了。

参考资料:

  • Dart (Flutter) serialize nested generics

炫意HTML5 » Flutter(Dart) 解析嵌套 JSON 为 model 对象的问题解决

Java基础教程Android基础教程