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
发起请求分为五步:
-
创建一个
HttpClient
:HttpClient httpClient = new HttpClient();
-
打开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); //可以直接添加输入流
-
等待连接服务器:
HttpClientResponse response = await request.close();
这一步完成后,请求信息就已经发送给服务器了,返回一个
HttpClientResponse
对象,它包含响应头(header)和响应流(响应体的Stream),接下来就可以通过读取响应流来获取响应内容。 -
读取响应内容:
String responseBody = await response.transform(utf8.decoder).join();
我们通过读取响应流来获取服务器返回的数据,在读取时我们可以设置编码格式,这里是utf8。
-
请求结束,关闭
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
对证书校验的逻辑如下:
如果请求的Https证书是可信CA颁发的,并且访问host包含在证书的domain列表中(或者符合通配规则)并且证书未过期,则验证通过。
如果第一步验证失败,但在创建HttpClient时,已经通过SecurityContext将证书添加到证书信任链中,那么当服务器返回的证书在信任链中的话,则验证通过。
如果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 进行网络请求。