Flutter学习 文件操作与网络请求

1. 文件操作

Dart 有 io 库,包含了文件读写,所以可以通过该库可以操作 Dart VM 下的脚本 和 Flutter。
和 Dart VM 相比, Flutter 一个比较重要的差异是文件系统路径不同, Dart VM 是运行在 PC 或 服务器操作系统下, 而 Flutter 是运行在 移动操作系统(Android 、 iOS)中,这会导致文件系统有一些差异。

1.1 App 目录

Android 和 iOS 的应用存储目录不同, PathProvider 插件提供了一种平台透明的方式来访问文件系统上的常用位置,该类当前支持访问两个文件系统位置:

  • 临时目录
    可以使用 getTemporaryDiretory() 来获取临时目录, 系统可以随时清除的临时目录。
    在 iOS 上,这对应用于 NSTemporaryDirectory() 返回的值, 在 Android 上, 这是 getCacheDir() 返回的值
  • 文档目录
    可以使用 getApplicationDocumentsDiretory() 来获取应用程序的文档目录,该目录用于存储只有自己可以访问的文件,只有当应用程序被写在时, 系统才会删除该目录。
    在 iOS 上对应的是 NSDocumentDirectory, Android 对应的是 /data/data/packagename/file 下 (内部存储路径)
  • 外部存储目录
    可以使用 getExternalStorageDirectory() 来获取外部存储目录。如 SD卡。
    iOS 系统因为不支持外部存储,所以 iOS 下调用这个方法,会抛出异常, 而 Android 则是对应 getExternalStorageDirectory, 即 /storage/emulated/0目录

1.2 示例

以计数器为例, 实现应用退出重启后可以恢复点击次数,也就是将计数保存在文件中。

第一步,我们需要引入 PathProvider 插件,pubspec.yaml文件中添加:

dependencies:
  path_provider: ^2.0.2

然后编写下面的代码:

class _FileOperationRouteState extends State<FileOperationRoute> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    // 从文件中读取点击次数
    _readCounter().then((int value) {
      setState(() {
        _counter = value;
      });
    });
  }

  Future<int> _readCounter() async {
    try {
      File file = await _getLocalFile();
      String contents = await file.readAsString();
      return int.parse(contents);
    } on FileSystemException {
      return 0;
    }
  }

  _getLocalFile() async {
    // 获取应用目录
    String dir = (await getApplicationDocumentsDirectory()).path;
    return File("$dir/counter.txt");
  }

  _incrementCounter() async {
    setState(() {
      _counter++;
    });
    // 将点击次数写到文件中去
    await (await _getLocalFile()).writeAsString("$_counter");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("文件操作")),
      body: Center(
        child: Text("点击了 $_counter 次"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _incrementCounter(),
        child: Icon(Icons.add),
        tooltip: "自增",
      ),
    );
  }
}

可以看到文件系统里面会有这个 counter.txt 文件,这个文件就是我们写入的内容:
在这里插入图片描述

2. 使用 HttpClient 请求

Dart IO库里封装了 Http 的请求类, 我们可以直接使用 HttpClient 来发起请求, 有五个步骤。

第一步: 创建一个 HttpClient

HttpClient httpClient = HttpClient();

第二步: 设置请求头等:

    // 创建请求头, 除了 getUrl 还可以使用 post、 put、delete等
    HttpClientRequest request = await httpClient.getUrl(uri);
    // 用于设置请求体
    request.headers.add("header-name", "header-value");
    // 用于设置 request body
    request.add(data);

第三步: 等待连接服务器:

 HttpClientResponse response = await request.close();

这一步完成后,请求信息就已经从服务器返回回来了, 它包含响应头和响应体,我们可以通过读取响应流来响应内容。

第四步:读取响应内容:

String responseBody = await response.transform(utf8.decoder).join();

第五步:关闭 HttpClient

httpClient.close();

2.1 示例

我们通过 HttpClient 来获取百度首页的内容,代码如下:

class _HttpClientRouteState extends State<HttpClientRoute> {
  bool _loading = false;
  String _text = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Column(
          children: [
            ElevatedButton(
                onPressed: _loading ? null : request, child: Text("获取百度首页")),
            Container(
              width: MediaQuery.of(context).size.width - 50.0,
              child: Text(_text.replaceAll(RegExp(r"\s"), "")),
            )
          ],
        ),
      ),
    );
  }

  request() async {
    setState(() {
      _loading = true;
      _text = "正在请求";
    });

    try {
      // 创建一个 HttpClient
      HttpClient httpClient = HttpClient();
      HttpClientRequest request =
          await httpClient.getUrl(Uri.parse("https://www.baidu.com"));
      HttpClientResponse response = await request.close();
      _text = await response.transform(utf8.decoder).join();
      // 打印响应头
      print(response.headers);

      // 关闭 client 后, 通过该 client 发起的所有请求都会中止
      httpClient.close();
    } catch (e) {
      _text = "请求失败:$e";
    } finally {
      setState(() {
        _loading = false;
      });
    }
  }
}

因为平台原因,这里就不放截图了,可以看到控制台输出的信息如下:
在这里插入图片描述

2.2 HttpClient 配置

HttpClient 有很多属性可以配置,常用的属性列表如官网给出的下图所示:
在这里插入图片描述

2.3 代理

可以通过 findProxy 来设置代理策略,例如我们要将所有请求通过代理服务器 192.18.19.245(乱写的) 发出去,可以设置:

httpClient.findProxy = (uri) {
        // 如果需要通过过滤 uri, 可以手动判断
        return "PROXY 192.18.19.245";
      };

如果不需要代理, 返回 “DIRECT” 既可以了

2.4 证书验证

Https 的连接会中, 客户端需要对自签名或者非CA办法的证书进行验证的, HttpClient 的证书校验逻辑如下:

  1. 如果请求的 Https 证书是可信 CA 颁发, 并且访问 host 包含在证书的 domain 列表中, 且未过期, 则 验证通过
  2. 如果第一步验证失败,但是在创建 HttpClient 时,已经通过了 SecurityContext 将证书添加到证书信任链中,那么当服务器返回的证书在信任链中, 则验证通过
  3. 如果1、2都验证失败,如果用户提供了 badCertificateCallback 回调,则会调用它, 如果回调返回为true, 则允许继续连接,否则终止连接

综上, 如果我们来做证书验证,可以通过2、3步来信任证书,其中第3个方法更加简单,我们来用一个示例来说明:

假设我们后台服务使用的是自签名证书,而证书的格式是 PEM 格式,我们将证书的内容保存在本地字符串中,那我们的校验逻辑如下:

      // 文件中读取的内容
      String PEM = "xxx";
      httpClient.badCertificateCallback = (X509Certificate cert, String host, int port) {
        if (cert.pem == PEM) {
          // 如果给的证书pem 和本地保存的PEM证书一致,则允许数据发送
          return true;
        }
        return false;
      };

其中 X509Certificate 是证书的标准格式,包含了证书除私钥外的所有信息。
另外的是,上面的示例没有校验host,host验证通常只是为了防止证书和域名不匹配

此外,我们还可以使用第二种的方式,将证书添加到证书信用链中,这样就会自动通过,而不会走到第三步:

      SecurityContext sc = SecurityContext();
      // file 为证书链
      sc.setTrustedCertificates(file);
      HttpClient httpClient = HttpClient(context: sc);

注意的是 setTrustedCertificates 方法设置的证书必须为 PEM 或者 PKCS12 格式, 如果证书格式为 PKCS12,则需将证书密码传入,这样代码会就会暴露密码证书,所以一般不建议客户端校验 PKCS12 格式的证书。

3. Dio库

dio 是一个强大的 Dart Http 请求库,支持 拦截器、FormData、Cookie 等, 就像是 Android 里的 OkHttp。 其官网链接:Dio Git官网

3.1 引入

在配置文件中引入 dio 库:

dependencies:
  dio: ^4.0.4

接下来就可以在代码中发起 Http 请求了

3.2 示例

发起一个 Get 请求:

Dio dio = Dio();
Response response = await dio.get("/text?id=12&name=rikka");
print(response.data.toString());

对于 Get 请求我们可以将 query 参数通过对象来传递,而不是写 ?xx=…&xx=…
上面代码等同于:

Response response = await dio.get("/text", queryParameters: {"id":12, "name": "rikka"});

就跟字典一样。

发起一个 POST 请求:

Response response = await dio.post("/text", data: {"id":12, "name": "rikka"});

发起多个并发请求:

Response response = (await Future.wait([dio.post("/info"), dio.get("/token")])) as Response;

下载文件:

Response response = await dio.download("https://baidu.com/", _savePath);

发起 FormData :

FormData formData = FormData.fromMap({"name": "rikka", "age": 24,"file1": UploadFileInfo(File("./upload.txt")), "upload1.txt") });
response = await dio.post("/info", data: formData);

如果发送的数据是 FormData, 则会自动将请求头设置为 contentType : multipart/form-data

Dio 本质是封装了 HttpClient, 所以它内部仍然是使用 HttpClient 来发起请求,所以 代理、请求认证、证书校验等和 HttpClient 是相同的,我们可以在 onHttpClientCreate 回调中设置, 例如:

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
        (client) {
      // proxy
      client.findProxy = (uri) {
        return "PROXY PROXY 192.18.19.245:8888";
      };
      // authorize
      client.badCertificateCallback =
          (X509Certificate cert, String host, int port) {
        if (cert.pem == PEM) {
          return true;
        }
        return false;
      };
    };

目前还不知道有没有别的接口来设置,只能通过这样看起来比较粗暴的实现, onHttpClientCreate 会在 dio 库创建 HttpClient 实例时回调。

下面我们来用 dio 实现一个请求:

  1. 在请求阶段弹出 loading
  2. 请求结束后,如果请求失败,则展示错误信息,如果成功,则将项目名称列表展示出来
class _DioRouteState extends State<DioRoute> {
  final Dio _dio = Dio();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body:  Container(
        alignment: Alignment.center,
        child: FutureBuilder(
          future: _dio.get("https://api.github.com/orgs/flutterchina/repos"),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            // call when request complete
            if (snapshot.connectionState == ConnectionState.done) {
              Response response = snapshot.data;
              if (snapshot.hasError) {
                return Text(snapshot.error.toString());
              }
              return ListView(
                children: response.data.map<Widget>((e) => ListTile(title: Text(e["full_name"]))).toList()
              );
            }
            return const CircularProgressIndicator();
          },
        ),
      ),
    );
  }
}

效果如下:
在这里插入图片描述

4. 分块下载器

下面通过 “Http分块”下载来演示下 dio 的具体用法。

实现器有这么一个实现思路:

  1. 先检测是否支持分块传输,如果不支持则直接下载,否则将剩余内容分块下载
  2. 各个分块下载时,保存到各自临时文件,等到所有分块下载完成后合并临时文件
  3. 删除临时文件

下面是代码实现:

  downloader() async{
    int total = 0;
    int firstChunkSize = 10;
    int maxChunk = 20;

    Response response = await downloadChunk(url, 0, firstChunkSize, 0);
    // 状态码为 206 表示支持分块下载
    if (response.statusCode == 206) {
      // 解析总文件长度
      total = int.parse(response.headers.value(HttpHeaders.contentRangeHeader)!.split("/").last);
      int reserve = total - int.parse(response.headers.value(HttpHeaders.contentLengthHeader)!);
      // 文件的总块数
      int chunk = (reserve / firstChunkSize).ceil() + 1;
      if (chunk > 1) {
        int chunkSize = firstChunkSize;
        if (chunk > maxChunk + 1) {
          chunk = maxChunk + 1;
          chunkSize = (reserve / maxChunk).ceil();
        }
        var futures = <Future>[];
        for (int i =0;i < maxChunk; ++i) {
          int start = firstChunkSize + i * chunkSize;
          futures.add(downloadChunk(url, start, start + chunkSize, i + 1));
        }
        // 等待所有分块全部下载完成
        await Future.wait(futures);
      }
      // 合并临时文件
      await mergeTempFiles(chunk);
    }
  }

下面来实现 downloadChunk 来下载第n块:

  /// no 表示当前是第几块
  Future<Response> downloadChunk(url, start, end, no) async {
    progress.add(0); // progress 用于记录每一块已接收数据的长度
    --end;
    return dio.download(url, savePath + "temp$no", // 临时文件的命名
        onReceiveProgress: createCallback(no), //创建回调进度
        options: Options(headers: {"rage": "bytes=$start-$end"}) //指定请求区间
        );
  }

接下来实现 mergeTempFiles 来合并多个下载到的临时文件:

  Future mergeTempFiles(chunk) async {
    File f = File(savePath + "temp0");
    IOSink ioSink = f.openWrite(mode: FileMode.writeOnlyAppend);
    // 合并临时文件
    for (int i = 1; i < chunk; i++) {
      File _f = File(savePath + "temp$i");
      await ioSink.addStream(_f.openRead());
      await _f.delete();   //删除临时文件
    }
    await ioSink.close();
    await f.rename(savePath);  //合并后的文件重命名为真正的名称
  }

下面来看看完全版代码:

  Future downloadWithChunks(
    url,
    savePath,
    ProgressCallback onReceiverProgress,
  ) async {
    const firstChunkSize = 102;
    const maxChunk = 3;

    int total = 0;
    var dio = Dio();
    var progress = <int>[];

    createCallback(no) {
      return (int received, _) {
        progress[no] = received;
        if (total != 0) {
          onReceiverProgress(
              progress.reduce((value, element) => value + element), total);
        }
      };
    }

    Future<Response> downloadChunk(url, start, end, no) async {
      progress.add(0);
      --end;
      return dio.download(url, savePath + "temp$no",
          onReceiveProgress: createCallback(no),
          options: Options(headers: {"rage": "bytes=$start-$end"})
          );
    }

    Future mergeTempFiles(chunk) async {
      File f = File(savePath + "temp0");
      IOSink ioSink = f.openWrite(mode: FileMode.writeOnlyAppend);
      // 合并临时文件
      for (int i = 1; i < chunk; i++) {
        File _f = File(savePath + "temp$i");
        await ioSink.addStream(_f.openRead());
        await _f.delete(); //删除临时文件
      }
      await ioSink.close();
      await f.rename(savePath); //合并后的文件重命名为真正的名称
    }

    Response response = await downloadChunk(url, 0, firstChunkSize, 0);
    if (response.statusCode == 206) {
      // 解析总文件长度
      total = int.parse(response.headers
          .value(HttpHeaders.contentRangeHeader)!
          .split("/")
          .last);
      int reserve = total -
          int.parse(response.headers.value(HttpHeaders.contentLengthHeader)!);
      int chunk = (reserve / firstChunkSize).ceil() + 1;
      if (chunk > 1) {
        int chunkSize = firstChunkSize;
        if (chunk > maxChunk + 1) {
          chunk = maxChunk + 1;
          chunkSize = (reserve / maxChunk).ceil();
        }
        var futures = <Future>[];
        for (int i = 0; i < maxChunk; ++i) {
          int start = firstChunkSize + i * chunkSize;
          futures.add(downloadChunk(url, start, start + chunkSize, i + 1));
        }
        await Future.wait(futures);
      }
      // 合并临时文件
      await mergeTempFiles(chunk);
    }
  }

下面我们就可以在顶层代码去调用了:

  main() async {
    var url = "http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg";
    var savePath = "./example/HBuilder.9.0.2.macosx_64.dmg";
    await downloadWithChunks(url = url, savePath = savePath,
        onReceiverProgress: (received, total) {
      if (total != -1) {
        print("下载:${(received / total * 1000).floor()}%");
      }
    });
  }

5. 使用 Socket 实现一个 Http 请求

我们之前使用的 Http 请求属于应用层协议,这些应用层协议都是通过 Socket Api 来实现的。高级编程语言中的 Socket 库都是操作系统的 Scoket API 的一层封装。

Flutter 的 Socket API 在 dart:io 包下,我们用一个 Socket 来实现简单的 HTTP 请求:

  _request() async {
    var socket = await Socket.connect("baidu.com", 80);
    // 根据http协议,发送请求头
    socket.writeln("GET / HTTP/1.1");
    socket.writeln("Host:baidu.com");
    socket.writeln("Connections:close");
    socket.writeln();
    
    await socket.flush(); //发送
    // 读取返回内容, 按照utf8解码为字符串
    String _response = await utf8.decoder.bind(socket).join();
    await socket.close();
    return _response;
  }

6. Json 转化成 Dart Model 类

在 Java / Kotlin 中,当我们需要将 Json 转化成数据Bean,Android Studio / IDE 有这样的插件可以做到直接转化, 并且有 Gson 这样的类库可以在代码运行时,帮我们做到解析。

但是在 Flutter 中没有 Java 这样的 Gson / Jackson 的类库,因为这样的库是需要在运行时反射,而 Flutter 是禁止这样做的,这是因为 运行时反射会干扰 Dart 的 tree shaking, 使用这个玩意可以在 release 版本中 “去除” 未使用的代码, 可以显著优化应用程序的大小。 而 反射 会默认应用到所有的代码,所以 tree shaking 相当于没用, 所以 Flutter 去掉了这个玩意, 正因如此, Flutter 才会禁止这个玩意,也没有提供官方的类库。 至于 IDE 的插件,质量是参差不齐,不为推荐

6.1 json_serializable

Flutter 官方给我们开发者提供了自动生成 Model 的库 json_serializable,可以在开发阶段为我们生成 JSON 序列化模板,我们就不用手写和维护序列化代码了。

首先需要导入,和其他库的导入不同, 该库除了需要引入常规的依赖项,还有两个 开发依赖 项,指的是不包含在我们应用程序源代码中的依赖项,而是一些开发过程中的辅助工具、脚本什么的:

dependencies:
  json_annotation: ^4.4.0

dev_dependencies:
  build_runner: ^2.1.7
  json_serializable: ^6.1.3

接下来我们来看看 User 类转化为一个 json_serializable, User 类含有两个属性: name、email

// user.g.dart 将在我们运行生成命令后自动生成, 目前还是报红的
part 'user.g.dart';

/// 这个注解是表示该类是需要生成 Model 类的
@JsonSerializable()
class User {
  User(this.name, this.email);

  String name;
  String email;

  // 不同的类使用不同的 mixin 即可
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  Map<String, dynamic> toJson() => _$UserToJson(this);
}

接下来通过在项目根目录下运行如下命令:

 flutter packages pub run build_runner build

就会帮我们构建:
在这里插入图片描述
这样就会帮我们生成了 user.g.dart 文件,这样我们就不用手动写序列化的代码了… 如下图所示
在这里插入图片描述

6.2 自动生成模板

上面的缺点就是要根据 Json 文件来写一遍Model类,过程是比较枯燥的、重复性的,所以 flutterchina 里实现了一个自动生成的模板, 只要将 Json 文件拷贝到一个 jsons 目录中,通过一行命令就能自动生成对应的 Dart Model 类, 原来是使用 Dart 来编写脚本,处理 Json 字符串,并帮忙实现 build_runner 脚本,感兴趣的同学可以看下官方文档: Json_model

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值