Flutter的⽂件操作和网络请求

1. 文件操作

Flutter中的文件操作一般使用Dart IO库来进行,而由于 Android 和 iOS 的应用存储目录不同,PathProvider 插件提供了一种平台透明的方式来访问设备文件系统上的常用位置。该类当前支持访问两个文件系统位置:

  • 临时目录: 可以使用 getTemporaryDirectory() 来获取临时目录; 系统可随时清除的临时目录(缓存)。在iOS上,这对应于NSTemporaryDirectory() 返回的值。在Android上,这是getCacheDir()返回的值。
  • 文档目录: 可以使用getApplicationDocumentsDirectory()来获取应用程序的文档目录,该目录用于存储只有自己可以访问的文件。只有当应用程序被卸载时,系统才会清除该目录。在iOS上,这对应于NSDocumentDirectory。在Android上,这是AppData目录。
  • 外部存储目录:可以使用getExternalStorageDirectory()来获取外部存储目录,如SD卡;由于iOS不支持外部目录,所以在iOS下调用该方法会抛出UnsupportedError异常,而在Android下结果是android SDK中getExternalStorageDirectory的返回值。

一旦你的Flutter应用程序有一个文件位置的引用,你可以使用dart:io API 来执行对文件系统的读/写操作。有关使用Dart处理文件和目录的详细内容可以参考Dart语言文档,下面我们看一个简单示例:

import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

class FileOperationRoute extends StatefulWidget {
  FileOperationRoute({Key key}) : super(key: key);

  @override
  _FileOperationRouteState createState() => new _FileOperationRouteState();
}

class _FileOperationRouteState extends State<FileOperationRoute> {
  int _counter;

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

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

  Future<int> _readCounter() async {
    try {
      File file = await _getLocalFile();
      // 读取点击次数(以字符串)
      String contents = await file.readAsString();
      return int.parse(contents);
    } on FileSystemException {
      return 0;
    }
  }

  Future<Null> _incrementCounter() async {
    setState(() {
      _counter++;
    });
    // 将点击次数以字符串类型写到文件中
    await (await _getLocalFile()).writeAsString('$_counter');
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(title: new Text('文件操作')),
      body: new Center(
        child: new Text('点击了 $_counter 次'),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
  }
}
运行结果

2. 网络请求 HttpClient

在实际开发中,我们免不了要跟网络请求打交道,此时我们可以使用HttpClient来发起请求。

2.1 HttpClient配置

HttpClient有很多属性可以配置,常用的属性列表如下:

属性 含义
idleTimeout 对应请求头中的keep-alive字段值,为了避免频繁建立连接,httpClient在请求结束后会保持连接一段时间,超过这个阈值后才会关闭连接。
connectionTimeout 和服务器建立连接的超时,如果超过这个值则会抛出SocketException异常。
maxConnectionsPerHost 同一个host,同时允许建立连接的最大数量。
autoUncompress 对应请求头中的Content-Encoding,如果设置为true,则请求头中Content-Encoding的值为当前HttpClient支持的压缩算法列表,目前只有"gzip"
userAgent 对应请求头中的User-Agent字段。

可以发现,有些属性只是为了更方便的设置请求头,对于这些属性,你完全可以通过HttpClientRequest直接设置header,不同的是通过HttpClient设置的对整个httpClient都生效,而通过HttpClientRequest设置的只对当前请求生效。

2.2 HttpClient使用

使用HttpClient发起请求分为五步:

  1. 创建一个HttpClient

     HttpClient httpClient = new HttpClient();
    
  2. 打开Http连接,设置请求头:

    HttpClientRequest request = await httpClient.getUrl(uri);
    

    这一步可以使用任意Http Method,如httpClient.post(...)httpClient.delete(...)等。如果包含Query参数,可以在构建uri时添加,如:

    Uri uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
        "xx":"xx",
        "yy":"dd"
      });
    

    通过HttpClientRequest可以设置请求header,如:

    request.headers.add("user-agent", "test");
    

    如果是post或put等可以携带请求体方法,可以通过HttpClientRequest对象发送request body,如:

    String payload="...";
    request.add(utf8.encode(payload)); 
    //request.addStream(_inputStream); //可以直接添加输入流
    
  3. 等待连接服务器:

    HttpClientResponse response = await request.close();
    

    这一步完成后,请求信息就已经发送给服务器了,返回一个HttpClientResponse对象,它包含响应头(header)和响应流(响应体的Stream),接下来就可以通过读取响应流来获取响应内容。

  4. 读取响应内容:

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

    我们通过读取响应流来获取服务器返回的数据,在读取时我们可以设置编码格式,这里是utf8。

  5. 请求结束,关闭HttpClient

    httpClient.close();
    

    关闭client后,通过该client发起的所有请求都会中止。

使用示例:

class HttpTestRoute extends StatefulWidget {
  @override
  _HttpTestRouteState createState() => new _HttpTestRouteState();
}

class _HttpTestRouteState extends State<HttpTestRoute> {
  bool _loading = false;
  String _text = "";

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints.expand(),
      child: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            RaisedButton(
                child: Text("获取掘金首页", style: TextStyle(color: Colors.red,fontSize: 13),),
                onPressed: _loading ? null : () async {
                  setState(() {
                    _loading = true;
                    _text = "正在请求...";
                  });
                  try {
                    //创建一个HttpClient
                    HttpClient httpClient = new HttpClient();
                    //打开Http连接
                    HttpClientRequest request = await httpClient.getUrl(
                        Uri.parse("https://juejin.im/"));
                    //使用iPhone的UA
                    request.headers.add("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1");
                    //等待连接服务器(会将请求信息发送给服务器)
                    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;
                    });
                  }
                }
            ),
            Container(
                width: MediaQuery.of(context).size.width-50.0,
                child: Text(_text.replaceAll(new RegExp(r"\s"), ""))
            )
          ],
        ),
      ),
    );
  }
}
运行效果

2.3 证书校验

Https中为了防止通过伪造证书而发起的中间人攻击,客户端应该对自签名或非CA颁发的证书进行校验。HttpClient对证书校验的逻辑如下:

  1. 如果请求的Https证书是可信CA颁发的,并且访问host包含在证书的domain列表中(或者符合通配规则)并且证书未过期,则验证通过。

  2. 如果第一步验证失败,但在创建HttpClient时,已经通过SecurityContext将证书添加到证书信任链中,那么当服务器返回的证书在信任链中的话,则验证通过。

  3. 如果1、2验证都失败了,如果用户提供了badCertificateCallback回调,则会调用它,如果回调返回true,则允许继续链接,如果返回false,则终止链接。

3. 网络请求 dio

Dart社区有一些第三方http请求库,用它们来发起http请求将会简单的多,其中的dio库人气较高,用户SDK示例中选用的也是这一库。

dio支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等,下面我们来看一些🌰

发起 GET 请求 :

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

对于GET请求我们可以将query参数通过对象来传递,上面的代码等同于:

response=await dio.get("/test",queryParameters:{"id":12,"name":"wendu"})
print(response);

发起一个 POST 请求:

response=await dio.post("/test",data:{"id":12,"name":"wendu"})

发起多个并发请求:

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

下载文件:

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

发送 FormData:

FormData formData = new FormData.from({
   "name": "wendux",
   "age": 25,
});
response = await dio.post("/info", data: formData)

如果发送的数据是FormData,则dio会将请求header的contentType设为“multipart/form-data”。当然你也可以自行设置contentType的类型:

Options options = Options(headers: t.getExtHeaders(), contentType: ContentType.parse("application/x-www-form-urlencoded"));

var response = await dio.post(t.getRequestUrl(), data: FormData.from(t.getBody()), options: options);

dio内部实际上仍然使用HttpClient发起请求,另外一个dio实例可以发起多个http请求,一般来说,APP只有一个http数据源时,dio应该使用单例模式。通过上述介绍可以看出dio的封装能够让我们更便捷的使用 HttpClient 进行网络请求。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容