Flutter 中的数据的获取

2,082 阅读7分钟

Flutter 中的数据的获取

APP 中的数据来源大致分为两部分,一是本地,包括 文件、数据库;二是通过网络从远处获取,今天一起来学习Flutter 如何获取这两类数据的。

Dark 单线程如何实现异步

在学习本地数据获取和网络数据获取之前,先了解Flutter 中如何处理完成异步的处理的。

异步:和同步相对,不等任务执行完,直接执行下一个任务。

首先我们了解到 Dark 是单线程,单线程是如何执行异步呢?

因为App 绝大多数时间都在等待。比如,等用户点击、等网络请求返回、等文件 IO 结果,等等。而这些等待行为并不是阻塞的。比如说,网络请求,Socket 本身提供了 select 模型可以异步查询;而文件 IO,操作系统也提供了基于事件的回调机制。所以,基于这些特点,单线程模型可以在等待的过程中做别的事情,等真正需要响应结果了,再去做对应的处理。因为等待过程并不是阻塞的,所以给我们的感觉就像是同时在做多件事情一样。但其实始终只有一个线程在处理你的事情。

和大多数语言一样,完成异步任务也是Event Loop来完成。把 需要在主线程响应的事件放入队列中,然后再主线程空闲的时候,不断的从 队列中取出消息,然后在主线程完成。

在Dark 中,其实有两个队列,一个是事件队列(Event Queue)、一个是微任务队列(Microtask Queue);微任务队列的优先级要比 事件队列要高,所以每次都会先检查 微任务队列,有就优先执行,没有了才从 事件队列取出事件进行处理。

image.png

了解了Dark 的异步处理机制后,看看在代码中如何实现的。

Dart 为 Event Queue 的任务建立提供了一层封装,叫作 Future。从名字上也很容易理解,它表示一个在未来时间才会完成的任务。把一个函数体放到了 Future 中就完成了从同步到异步的转换。

void syncDemo1() {
  Future(() => print("print future1"));//a
  print("print 1");//b

  Future(() => print("print 2"))//c
    ..then((value) => print("print 3"))//d
    ..then((value) => print("print 4"));//e
}
print 1
print future1
print 2
print 3
print 4

调用的顺序是:

  • a 添加到任务队列;
  • b 在主线程,执行执行;
  • c 添加到任务队列;这时候在主线程没有任务了,从任务队列取出 a,并执行
  • 执行a 后,主线程没有任务,还是从任务队列取出c 执行,then 后面的函数会依次同步执行;

异步函数

对于一个异步函数来说,其返回时内部执行动作并未结束,因此需要返回一个 Future 对象,供调用者使用。调用者根据 Future 对象,来决定:

  • 是在这个 Future 对象上注册一个 then,等 Future 的执行体结束了以后再进行异步处理;
  • 还是一直同步等待 Future 执行体结束。对于异步函数返回的 Future 对象,如果调用者决定同步等待,则需要在调用处使用 await 关键字,并且在调用处的函数体使用 async 关键字。
void syncFunDemo() async{
  Future<String> future() =>  Future<String>.delayed(Duration(seconds: 3),()=>"hello 2021");
  // future.then((value) => print("获取到异步数据$value"));//等待完成后,执行then里面的函数
  print("获取到异步数据"+ (await future()));//同步等待
}

为什么要加上 async?

因为 Dart 中的 await 并不是阻塞等待,而是异步等待。Dart 会将调用体的函数也视作异步函数,将等待语句的上下文放入 Event Queue 中,一旦有了结果,Event Loop 就会把它从 Event Queue 中取出,等待代码继续执行。

Flutter 中的网络请求

在了解了异步操作之后,趁热打铁,学习flutter 中的网络请求。

网络与服务端数据交互时,不可避免地需要用到三个概念:定位、传输与应用。

其中,定位,定义了如何准确地找到网络上的一台或者多台主机(即 IP 地址);传输,则主要负责在找到主机后如何高效且可靠地进行数据通信(即 TCP、UDP 协议);而应用,则负责识别双方通信的内容(即 HTTP 协议)。

一般的网络请求框架中,依次http 网络调用 可以分为以下几个部分:

  • 创建网络调用实例 client,设置通用请求行为(如超时时间);
  • 构造 URI,设置请求 header、body;
  • 发起请求, 等待响应;
  • 解码响应的内容。

在 Flutter 中,Http 网络编程的实现方式主要分为三种:dart:io 里的 HttpClient 实现、Dart 原生 http 请求库实现、第三方库 dio 实现。

HttpClient

Dart 原生 http 请求库实现

第三方库 dio 实现

HttpClient 和 http 使用方式虽然简单,但其暴露的定制化能力都相对较弱,很多常用的功能都不支持(或者实现异常繁琐),比如取消请求、定制拦截器、Cookie 管理等。因此对于复杂的网络请求行为,我推荐使用目前在 Dart 社区人气较高的第三方 dio 来发起网络请求。

  • 加入dio 依赖
dio: 3.0.10
  • 简单的get 请求
void getRequest() async {
  //创建网络调用示例
  Dio dio = Dio();

  //设置URI及请求user-agent后发起请求
  var response = await dio.get("https://wanandroid.com/wxarticle/chapters/json",
      options: Options(headers: {"user-agent": "Custom-UA"}));

  //打印请求结果
  if (response.statusCode == HttpStatus.ok) {
    print(response.data.toString());
  } else {
    print("Error: ${response.statusCode}");
  }
}
  • 同时发起多个请求
// 同时发起多个请求
void getRequest2() async {
  //创建网络调用示例
  Dio dio = Dio();
//同时发起两个并行请求
  List<Response> responseX = await Future.wait([
    dio.get("https://wanandroid.com/wxarticle/chapters/json"),
    dio.get("https://www.wanandroid.com/article/list/0/json")
  ]);

//打印请求1响应结果
  print("Response1: ${responseX[0].toString()}");
//打印请求2响应结果
  print("Response2: ${responseX[1].toString()}");
}
  • 添加拦截器
// 添加拦截器
void getRequest3() async {
  //创建网络调用示例
  Dio dio = Dio();

  //增加拦截器
  dio.interceptors.add(InterceptorsWrapper(onRequest: (RequestOptions options) {
    //为每个请求头都增加user-agent
    options.headers["user-agent"] = "Custom-UA";
    print("interceptor request ${options.uri}");
    print("interceptor request ${options.headers}");
    print("interceptor request ${options.data}");
    //放行请求
    return options;
  }, onResponse: (Response response) {
    print("interceptor response ${response.data.toString()}");
  }));

//增加try catch,防止请求报错
  try {
    await dio.get("https://wanandroid.com/wxarticle/chapters/json");
  } catch (e) {
    print(e);
  }
}

以上都是dio 简单的网络请求示例,更多的高级用法,可以到 GitHub 去看看。

Json 解析

在完成了网络请求后,得到的数据我们还不能直接使用,要先解析服务器给我们返回的数据。而json 是常用的服务端和客户端传输一种数据格式。

在拿到服务端返回的json 数据后,如何解析呢?

由于 Flutter 不支持运行时反射,因此并没有提供像 Gson、Mantle 这样自动解析 JSON 的库来降低解析成本。在 Flutter 中,JSON 解析完全是手动的,开发者要做的事情多了一些,但使用起来倒也相对灵活。

自动解析:使用 dart:convert 库中内置的 JSON 解码器,将 JSON 字符串解析成自定义对象的过程。使用这种方式,我们需要先将 JSON 字符串传递给 JSON.decode 方法解析成一个 Map,然后把这个 Map 传给自定义的类,进行相关属性的赋值。

  • json 解析
class Student{
  //属性id,名字与成绩
  String id;
  String name;
  int score;
  //构造方法
  Student({
    this.id,
    this.name,
    this.score
  });
  //JSON解析工厂类,把map 解析成model 对象
  factory Student.fromJson(Map<String, dynamic> parsedJson){
    return Student(
        id: parsedJson['id'],
        name : parsedJson['name'],
        score : parsedJson ['score']
    );
  }
}

void parseJson1(){

var jsonString = """{ "id":"1234", "name":"周结", "score" : 95}""" ;
    //jsonString为JSON文本
    final jsonResponse = json.decode(jsonString);
    Student student = Student.fromJson(jsonResponse);
    print(student.name);

}

json 数据解析生成类中,都有一个 json 解析工厂;要是里面嵌套 比较多层,估计解析起来也挺累的,这样的事情交给插件就可以了。 在 Android studio plugins 中搜索 FlutterJsonBeanFactory安装该插件,然后重启studio。

QQ20210325-211230.gif

/// 使用 FlutterJsonBeanFactory插件实现 json转model
void parseJson2() {
  var jsonString = """{ "id":"123", "name":"张三", "score" : 95}""";
  //jsonString为JSON文本
  final jsonResponse = json.decode(jsonString);
  UserEntity student = JsonConvert.fromJsonAsT(jsonResponse);
  print(student.name);
}

FlutterJsonBeanFactory 插件帮我们做了好多事情,让我们更能专注于开发。

Flutter 中本地数据管理

通过上面的学习,我们了解了在dart 中的网络请求和json 解析,完成了从远程获取数据的学习,接下来看看 flutter 如何处理本地数据的。

Flutter 提供了三种数据持久化方法,即文件、SharedPreferences 与数据库。

文件

文件是存储在某种介质(比如磁盘)上指定路径的、具有文件名的一组有序信息的集合。从其定义看,要想以文件的方式实现数据持久化,我们首先需要确定一件事儿:数据放在哪儿?这,就意味着要定义文件的存储路径。

Flutter 提供了两种文件存储的目录,即临时(Temporary)目录与文档(Documents)目录:

  • 临时目录是操作系统可以随时清除的目录,通常被用来存放一些不重要的临时缓存数据。这个目录在 iOS 上对应着 NSTemporaryDirectory 返回的值,而在 Android 上则对应着 getCacheDir 返回的值。
  • 文档目录则是只有在删除应用程序时才会被清除的目录,通常被用来存放应用产生的重要数据文件。在 iOS 上,这个目录对应着 NSDocumentDirectory,而在 Android 上则对应着 AppData 目录。

// 文件的读取,要先依赖 path_provider: ^2.0.1

class LocalDataDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("本地数据读取"),
      ),
      body: ListView(
        children: [
          ListTile(
            title: Text("本地文件读写"),
            onTap: () => Navigator.pushNamed(context, "LocalFileDemo"),
          ),ListTile(
            title: Text("SharePreferenceDemoDemo"),
            onTap: () => Navigator.pushNamed(context, "SharePreferenceDemo"),
          )
        ],
      ),
    );
  }
}

class LocalFileDemo extends StatelessWidget {
//创建文件目录
  Future<File> get _localFile async {
    final directory = await getApplicationDocumentsDirectory();
    final path = directory.path;
    return File('$path/test.txt');
  }

//将字符串写入文件
  Future<File> writeContent(String content) async {
    final file = await _localFile;
    return file.writeAsString(content);
  }

//从文件读出字符串
  Future<String> readContent() async {
    try {
      final file = await _localFile;
      String contents = await file.readAsString();
      return contents;
    } catch (e) {
      return "";
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("本地文件读写"),
      ),
      body: Center(
        child: Column(
          children: [
            MaterialButton(
              onPressed: () {
                writeContent("hello jay 2021");
              },
              child: Text("写入数据"),
            ),
            MaterialButton(
              onPressed: () async {
                print("从文件读取到的数据:${await readContent()}");
              },
              child: Text("读取数据"),
            )
          ],
        ),
      ),
    );
  }
}

sharePreference

使用前先添加依赖 shared_preferences: ^2.0.5,接下来的使用就很简单了。

// sharePreference 适合存储数据量比较小的键值对,要先依赖 shared_preferences: ^2.0.5
class SharePreferenceDemo extends StatelessWidget {
  String spName = "sp_name";

  _spSaveString() async {
    SharedPreferences.setMockInitialValues({});// 需要添加,否则会报错 No implementation found for method getAll on channel plugins.flutter.io/shared_preferences
    var sharePreference = await SharedPreferences.getInstance();
    await sharePreference.setString(spName, "this is sharePreference");
  }

  _readSpString() async {
    var sharePreference = await SharedPreferences.getInstance();
    print("这是从sharePreference 读取的数据:${sharePreference.getString(spName)}");
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("sharePreferenceDemo"),
      ),
      body: Center(
        child: Column(
          children: [
            MaterialButton(
              onPressed: () {
                _spSaveString();
              },
              child: Text("写入数据"),
            ),
            MaterialButton(
              onPressed: () {
                _readSpString();
              },
              child: Text("读取数据"),
            )
          ],
        ),
      ),
    );
  }
}

当然 sharePreference 提供了基本数据类型的存储,更多api 可以看看 sharePreference 的 GitHub

数据库

除了上面两种存储方式外,Flutter 还提供了数据库的存储,适用于需要持久化大量格式化后的数据,并且这些数据还会以较高的频率更新。与文件和 SharedPreferences 相比,数据库在数据读写上可以提供更快、更灵活的解决方案。

下面看一个小案例,了解数据库的使用; 首先需要 依赖 sqflite: ^1.3.0

参考链接

www.jianshu.com/p/2eafae001…