Dreamer2q Blog
见到,不如不见
Dreamer2q

Code is cheap, talk is expensive

64日志

Flutter中LocalStorage的小坑

创建于 2021-02-20 共 1426 字,阅读约 6 分钟
浏览 17评论 1


App需要在本地储存许多数据,储存数据有很多方案,例如 ​SharedPreference ​LocalStorage​ 或者直接储存文件,更进一步还有 ​sqlite​ 可以选择。


在pub.dev上面, ​sqflite​ 获取1.3k多个👍,可见其受欢迎程度。


但是对于我做的小App来说,需要储存的数据非常的少,基本上都是后台传输的​json​ 数据。


为了更好的处理这些数据,我都会使用一个 ​JsonToDart​ 的插件,来帮我快速完成 ​model​ 的编写。



例如,一个典型的API返回格式


{
  "code":200,
  "msg":"success",
  "data":null
}


会生成这样的 ​model​ 


class ApiResponse {
  int code;
  String msg;
  dynamic data;

  ApiResponse({this.code, this.msg, this.data});

  ApiResponse.fromJson(Map<String, dynamic> json) {
    code = json['code'];
    msg = json['msg'];
    data = json['data'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['code'] = this.code;
    data['msg'] = this.msg;
    data['data'] = this.data;
    return data;
  }
}


这样就将,json格式的返回数据,和dart中的类对于起来了。


之所以要这样搞,是因为flutter中将反射禁用了,目的为了方便 ​TreeShaking 


为了将 ​json​ 字符串转换成 ​dart​ 中的对象,一般需要

var jsonMap = jsonDecode(jsonStr);  //将jsons字符串转换成Map格式
var code = jsonMap['code'];             //通过Map的方式使用json数据,不方便,易错。
var apiRes = ApiResponse.fromJson(jsonMap); //通过ApiResponse的构造函数来生成对应的对象
var code2 = apiRes.code;                    //直接使用json数据,方便快捷。

而这 ​jsonDecode​ 一步,Dio会帮我们完成,只需要HTTP返回类型是 ​application/json​ 即可。


储存JSON对象


有些重要数据是需要储存的,比如说用户的个人信息,一般情况下是不会改变的,不需要每次都去请求再显示在界面上,不然会收到网络波动导致较差的体验。


考虑简单易用,我就使用 ​LocalStorage​ 来作为储存JSON对象的方法了。

它有两个很重要的方法


  • Future<void> setItem(String key, value, [Object toEncodable(Object nonEncodable))
  • dynamic getItem(String key)
//使用LocalStorage

localStorage.setItem('itemKey', myObject);

localStorage.getItem('itemKey');


使用LocalStorage会在App的数据目录下 ​app_flutter​ 下面出现 ​LocalStorage​ 文件,可以复制到本地打开看一下,它是以 ​json​ 格式进行储存的数据。


对于非基本格式的数据,需要有 ​toJson​ 的方法,将自定义数据转换成 ​Map​ 嵌套的形式。否则的话,会报

[ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: Converting object to an encodable object failed: Instance of 'XXX'

表明你的数据类型无法进行转换。



LocalStorage中 ​setItem​ 会尝试调用 ​toJson​ 的方法,来进行转换数据。


Future<void> setItem(
    String key,
    value, [
    Object toEncodable(Object nonEncodable),
  ]) async {
    var data = toEncodable != null ? toEncodable(value) : null;
    if (data == null) {
      try {
        data = value.toJson();
      } on NoSuchMethodError catch (_) {
        data = value;
      }
    }

    await _dir.setItem(key, data);

    return _flush();
  }


如果是 ​List​ 或者其它无 ​toJson​ 方法的对象,也没有关系,只要 ​jsonEncode​ 不会失败就行了。


最终 ​LocalStorage​ 会将数据按照相应的 ​key​ 在文件中保存成 ​json​ 格式数据。


这就和正常的 ​json​ 数据没啥区别了。


读取数据


这里的数据读取有两种情况


  1. 数据从文件中获取,这种情况返回的数据是 ​Map​ 类型 ​json​ 数据,可以使用 ​fromJson​ 来进行转换。
  2. 数据直接返回,这种情况下就是直接返回之前 ​setItem​ 传入的数据。


一开始我以为 ​setItem​ 后数据全部变成 ​Map​ 格式, ​getItem​ 返回也是这种格式。所有,在使用到的时候根本没有考虑它没有发生变化,直接尝试 ​fromJson​ 来解析数据。直接就是类型不匹配,连方式都进不去就挂了。


为了减少负担,不用每次使用时都进行判断,我在写入之前就把它转换成 ​Map​ 格式的进行存储,这样就不要担心数据返回的时候类型不一致的问题了。


转换代码如下


 await StorageManager.localStorage.setItem(_key, data, (obj) {
      dynamic o = obj;
      if (obj is Set) {
        o = obj.toList();
      }
      if (o is List) {
        var json = [];
        (o as List).forEach((item) {
          try {
            json.add(item.toJson());
          } on NoSuchMethodError catch (_) {
            json.add(item);
            debugPrint('$item not implement toJson method');
          }
        });
        o = json;
      }
      return o;
    });


思路非常简单,对于 ​Set​ 这样发生进行 ​jsonEncode​ 的数据,直接转发成List来处理,如果遇到List就处理里面每条数据。当然这缺点是 ​List​ 无法嵌套 ​List​ 。不过这样方案对我来说完全够了。