Dart高级(一)——泛型与Json To Bean

5,356 阅读9分钟

Flutter 发布到现在, 越来越多人开始尝试使用 Dart 来完成部分功能;

Dart 的前生今世一类的话题,这里就不展开了,只要知道 Fluttergoogle 推出跨平台方案就好,至少不必担心Dart性能生态问题(如果google真的想支持的话).

先看一下最终的效果显示:

Dart 语言

根据最新的语言排行榜...不好意思,没找到Dart,就目前来看,在前端没拼过JS,在其他领域好像也就目前 Flutter 在支持,不过相信随着Flutter盛行,情况会越来越好的.

Dart 语言有两种模式:JIT,AOT;这个和之前 Android/Java 情况很相似,JVM 一直采用解释执行的方式, 直到 ART 出现,Android端才开始 AOT 模式;

JIT比较方便,不需要提前编译,AOT 运行时比较快,而 Flutter 则在 debug 和 release 时,分别适用不同模式,保证开发效率以及应用流畅.

说了那么多,还没有到正题,接下来,我们来看下 Dart 中比较 "特殊" 的特性:泛型

Dart泛型 与 其他泛型

首先来看一段关于泛型的 Dart 代码(排列组合:C^2_5):

  List<int> a = <int>[];
  List<num> b = <num>[];
  List<dynamic> c = <dynamic>[];
  List<Object> d = <Object>[];
  List e = [];

  print(a.runtimeType == b.runtimeType);  // false
  print(a.runtimeType == c.runtimeType);  // false
  print(a.runtimeType == d.runtimeType);  // false
  print(a.runtimeType == e.runtimeType);  // false
  print(b.runtimeType == c.runtimeType);  // false
  print(b.runtimeType == d.runtimeType);  // false
  print(b.runtimeType == e.runtimeType);  // false
  print(c.runtimeType == d.runtimeType);  // false
  print(c.runtimeType == e.runtimeType);  // true
  print(d.runtimeType == e.runtimeType);  // false

虽然都是 List 类型,但泛型不同,获取到的runtimeType都不同,由此可知:

Dart中泛型运行时依旧存在.

相比之下,Java中的泛型就比较随意了:

List<Integer> a = new ArrayList<>();
List<Object> b = new ArrayList<>();
List c = new ArrayList<>();

java泛型在运行时,会被擦除,所以上面的a,b,c判断时都属于List类型.

再回到前面 Dart 部分,可以看到只有变量ce的运行时类型相同,并且如果使用编译器的话,就可以发现:

List c = <dynamic>[];

List<dynamic> 其实就是 List,两者是一样的.

在知道了dart的泛型特性后,不禁会思考:List<int>List,或者说和List<dynamic>是什么关系?和List呢?

还是开始的dart示例,在后面我们添加几行判断(变量b的类型为List\<num\>)

  print(b is List<int>);  //  false
  print(b is List<num>);  //  true
  print(b is List<Comparable>);  //  true
  print(b is List);  //  true

Comparablenum类实现的抽象类,根据验证结果可知:

dart泛型List时,泛型为父类的List是泛型为子类List的父类

这个有点绕,理解这个意思就行;其实看到这里,如果之前有过java开发经验的话,就会发现这个其实和java中的数组很相似,参考Java中的数组和List集合以及类型强转

这个图说明了java中数组之间是存在继承关系的,并且和实际类之间的继承关系相似.


接下来我们看一下泛型类中泛型的关系;

void main() {
  var aa = AA.name("123");
  aa.test();
}

class AA<T> {
  T field;

  AA.name(this.field);

  void test() {
    print("".runtimeType); //String
    print(T); //String
    print(field.runtimeType == T);  //true
  }
}

相信大家对这段代码以及结果都没有什么疑问,泛型在传递之后,到了类AA中仍然为String类型,但下面这段代码就不同了:

void main() {
  var aa = AA.name([]);
  aa.test();
}

class AA<T> {
  T field;

  AA.name(this.field);

  void test() {
    print([].runtimeType); //List<dynamic>
    print(T); //List<dynamic>
    print(field.runtimeType == T);  //false
  }
}

我们在创建类时,指定泛型为List(或者说是List<dynamic>),然后在AA中判断,发现泛型Tfield的运行时类型不同,虽然toString是一样的,但如果打印hashCode可以发现,对应着两个不同的Type.

加入我们传入一个map类型,结果更是不如人意

void main() {
  var aa = AA.name({"ss":1});
  aa.test();
}

class AA<T> {
  T field;

  AA.name(this.field);

  void test() {
    print(field.runtimeType); // _InternalLinkedHashMap<String, int>
    print(T); // Map<String, int>
    print(field.runtimeType == T); // false
  }
}

实际类型是泛型的一个实现类;

那假如传入一个自定义泛型类呢?

void main() {
  var aa = AA.name(BB<num>());
  aa.test();
}

class AA<T> {
  T field;

  AA.name(this.field);

  void test() {
    print(field.runtimeType); // BB<num>
    print(T); //  BB<num>
    print(field.runtimeType == T); // true
  }
}

class BB<T> {}

还好,自定义泛型类时,运行时runtimeType与泛型Type是相同的

关于dart泛型的探讨只进行到这里,大概总结一下:

  1. 泛型在运行时保留
  2. List泛型之间存在关系,可以通过 is 进行判断,如 field is List<num>
  3. List泛型即便toString方法返回相同的值,也可能是两个不同的Type

有了上面的论断,接下来进入正题

dart-bean

bean只是我们通用的一种class类的说明,用于表明数据模型.

这里还是先看一下其他语言中的bean;

先看java中通常的做法:

public class TestBean {
    private String username;
    private boolean isVip;

    public String getUsername() {
        return username;
    }

    public TestBean setUsername(String username) {
        this.username = username;
        return this;
    }

    public boolean isVip() {
        return isVip;
    }

    public TestBean setVip(boolean vip) {
        isVip = vip;
        return this;
    }
}

kotlin中表示形式会简单的多:

data class TestBean(
        var username: String? = null,
        var isVip: Boolean = false
)

其实bean的代码提现不是关键,重要的地方在于,如何方便的为bean赋值操作bean,因为 JVM 语言可以进行反射,因此,在获取到后台传入的json格式数据时,我们可以很方便的通过一些库完成自动赋值操作:

var bean = Gson().fromJson("{\"username\":\"www\",\"isVip\":false}",TestBean::class.java)

在dart中,bean的代码表现形式和其他语言基本相同,都是在单独的class中声明成员变量:

class TestBean {
  String username;
  bool isVip;
}

如果只是单纯的dart项目,我们仍然可以通过镜像功能来为bean赋值:

import 'dart:mirrors';

class TestBean {
  String username;
  bool isVip;
}

void main() {
  Map<String, dynamic> json = {
    "username": "www",
    "isVip": false,
  };
  var class_bean = reflectClass(TestBean);
  var obj = class_bean.newInstance(Symbol.empty, []).reflectee;
  var instance_bean = reflect(obj);
  class_bean.declarations.forEach((key, value) {
    if (value is VariableMirror) {
      var key_string = RegExp("^Symbol\\(\"(.+?)\"\\)\$")
          .firstMatch(key.toString())
          .group(1);
      instance_bean.setField(key, json[key_string]);
    }
  });
  print("${obj.username}  ${obj.isVip}"); // www  false
}

虽然dart的镜像功能看起来很是"不爽",但确实是有这个实现的.

不过有些不幸的是,在Flutter中,dart不允许使用镜像功能,具体原因可以参考Flutter实战中提到一段话:

很多人可能会问Flutter中有没有像Java开发中的Gson/Jackson一样的Json序列化类库?答案是没有!因为这样的库需要使用运行时反射,这在Flutter中是禁用的。运行时反射会干扰Dart的tree shaking,使用tree shaking,可以在release版中“去除”未使用的代码,这可以显著优化应用程序的大小。由于反射会默认应用到所有代码,因此tree shaking会很难工作,因为在启用反射时很难知道哪些代码未被使用,因此冗余代码很难剥离,所以Flutter中禁用了Dart的反射功能,而正因如此也就无法实现动态转化Model的功能。

因此,如果想在Flutter中实现bean,就需要其他的一些技巧.

flutter dart-bean

目前大多数情况下,都是在bean类中添加命名构造函数,然后通过工具自动生成部分代码帮助解析和构建bean.

例如上面的bean类会对应生成如下代码:

class Response {
  String username;
  bool isVip;

  Response.fromJsonMap(Map<String, dynamic> map)
      : username = map["username"],
        isVip = map["isVip"];

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['username'] = username;
    data['isVip'] = isVip;
    return data;
  }
}

即多添加两部分内容:

  1. 命名构造函数:Response.fromJsonMap,参数为Map<String, dynamic>类型,用于由 json 生成 bean;为json反序列化
  2. toJson 方法,用于将对象序列化为json字符串

通过添加两个方法,可以实现json串及bean的相互转换.不过如果每次都得手写那会很麻烦,因为可能需要的字段特别多,一般这种情况都需要用工具来完成.

官方也提供了相应的库:json_serializable,不过如果我们习惯了 GsonFormat 的快捷,不免会对这种方法感到不满,并且目前针对 dart 生成bean有很多不得不考虑的问题,比如泛型,嵌套内部类等等,这些情况下,我们无法通过一般的命令行或者工具直接生成.

这里我改写了 ,把其中的java类型部分修改为了dart类型,同时修改了生成方法:

插件地址在jetbrains-plugins-JsonToDartBean;

Package包地址在json_holder_impl;

详细的使用教程请参考github:json-to-dart-bean;

效果大概是这样:

使用方法很简单,只要按照教程导入包,然后安装插件就好,这里进行几点说明:

1. 插件功能

因为插件修改自GsonFormat,因此命名规范等都沿用了之前的定义,功能主要包括:

  1. 在dart文件中,使用快捷键alt + d,会根据粘贴的json 生成 bean

  2. json 支持创建时修改类型,这点和 GsonFormat 相同

  3. json 支持嵌套内部类,如果内部类不想创建新的,想使用已存在的某个class,也可以通过创建时修改类型来达到目的:

    • 假如默认要生成的类型是这样:

    • 我们可以修改类型或者去掉勾选,变成这样:

  4. 支持两层List连续嵌套,比如这种情况,会生成List<List>类型的变量:

    {
        "values":[
            [
            1,2,4
            ]
        ]
    }
    

2. 库特点

通过该插件简单的生成较短的代码进行查看:

//******************************************************************
//**************************** Generate By JsonToDartBean  **********
//**************************** Thu Jun 06 18:33:38 CST 2019  **********
//******************************************************************

import 'package:json_holder_impl/json_holder_impl.dart';

class FirstBean with JsonHolderImpl<FirstBean> {
  /// [key : value] => [name : www]
  String get name => getValue("name");
  set name(String value) => setValue("name", value);

  FirstBean.fromJson([Map<String, dynamic> json]) {
    fromJson(json);
  }

  @override
  JsonHolderImpl<FirstBean> provideCreator(Map<String, dynamic> json) {
    return FirstBean.fromJson(json);
  }

  @override
  List<FirstBean> provideListCreator() {
    return <FirstBean>[];
  }

  @override
  List<List<FirstBean>> provideListListCreator() {
    return <List<FirstBean>>[];
  }

}

可以看到类中其实没有生成任何的成员域,所有变量都同等的被get 和 set方法替代,这种方式相对于目前普遍的bean构建方法有一个好处,那就是变量值只有在真正调用的时候才会去解析,能在一定程度上加快运行速度(同样在父类中处理了类型转换,并且可自定义,具体自定义方式参考上面提到的github教程即可).

代码中只看到了fromJson方法,toJson方法 定义在了父类JsonHolderImpl中,直接调用即可.

注: 代码不可用于商业用途 注: 如有 插件 GsonFormat 涉及到的版权问题,请及时告知