flutter dio上传文件、下载文件总结

来源:blog.csdn.net 更新时间:2023-05-25 21:55

本文章将讲述
1.使用dio发送基本的get请求
2.使用dio发送get请求的传参方式
3.解析响应json数据
4.使用dio发送post请求并提交FormData参数
5.使用dio发送post请求并提交json参数
6.使用dio上传文件并实现进度监听
7.使用dio下载文件并实现进度监听
8.配制dio的拦截器
9.配制dio网络代理抓包
10.配制dio请求header以及公共请求参数
11.dio 取消网络请求

1 引言

dio用来在flutter跨平台开发中访问网络的框架,在使用的时候,我们首先是引入依赖

dependencies:
 dio: 3.0.9
  • 1
  • 2

也可以访问国内pub仓库来查看 dio的最新版本。

一般添加依赖如下所示

dependencies:
  dio: ^3.0.9
 

两种写法的差别是 ^在每次 flutter pub get 是会有小版本的自动升级,不添加这个符号就不会有自动小升级

2 Dio get请求
2.1 Dio get 请求无参数
  //get请求无参数
  void getRequestFunction1() async {
    ///创建Dio对象
    Dio dio = new Dio();
    ///请求地址 获取用户列表
    String url = "http://192.168.0.102:8080/getUserList";
    ///发起get请求
    Response response = await dio.get(url);
    ///响应数据
    var data = response.data;

    setState(() {
      result = data.toString();
    });
  }

数据响应结果

{
    "code": 200,
    "data": [
        {
            "id": 3,
            "userName": "测试人员",
            "realName": "张三",
            "age": 22
        }
    ],
    "message": "请求成功"
}
 

断点调试如下
在这里插入图片描述

2.2 Dio get 请求有参数
  ///get请求有参数
  ///根据用户ID来获取用户信息
  void getRequestFunction2() async {
    ///用户id
    int userId =3;
    ///创建 dio
    Dio dio = new Dio();
    ///请求地址
    ///传参方式1
    String url = "http://192.168.0.102:8080/getUser/$userId";
    ///传参方式2 
    String url2 = "http://192.168.0.102:8080/getUser?userId=$userId";
    ///传参方式 3
    String url3 = "http://192.168.0.102:8080/getUser";

    Map<String,dynamic> map = Map();
    map["userId"]= userId;
    ///发起get请求
    Response response = await dio.get(url3,queryParameters: map);

    ///响应数据
    Map<String,dynamic> data = response.data;
    /// 将响应数据解析为 UserBean
    UserBean userBean = UserBean.fromJson(data);
  }

}

在上述代码中,传参方式1与传参方式2是在请求链接中拼接参数,请求方式3是将参数放在一个 map 中,然后通过 Dio 的queryParameters 来配制参数,上述返回的数据结构为

{
    "code": 200,
    "data": {
        "id": 3,
        "userName": "测试人员",
        "realName": "张三",
        "age": 22
    },
    "message": "请求成功"
}

断点调试
在这里插入图片描述
对于这里使用到的数据模型 UserBean 对象来说


class UserBean{
  String userName;
  String realName;
  int age;
  int id;

  static UserBean fromJson(Map<String,dynamic> rootData){
    ///解析第一层
    Map<String,dynamic> data = rootData["data"];
    ///解析第二层
    UserBean userBean = new UserBean();

    userBean.id = data["id"];
    userBean.age = data["age"];
    userBean.userName= data["userName"];
    userBean.realName = data["realName"];
    return userBean;
    
  }
}

对于 UserBean 中的数据解析如下图所示

在这里插入图片描述
在这里插入图片描述

3 Dio post请求
2.1 Dio post 请求提交 FormData 表单数据

FormData 将提交的参数 name与value进行组合,实现表单数据的序列化,从而减少表单元素的拼接
也可以这样来描述:FormData 接口提供了一种表示表单数据的键值对的构造方式,通过FormData发出的请求编码类型被设为 “multipart/form-data”,而在网络请求访问中,通过 Content-Type 来记录这个值,可以理解为Content-Type 表示具体请求中的媒体类型信息。

而我们在实际开发中常用的 Content-Type如下

multipart/form-data
application/json     JSON数据格式
application/x-www-form-urlencoded 表单数据格式

下面我们将使用 dio 来发起一个post请求,提交参数的格式为 FromData

  void postRequestFunction() async {
    ///创建Dio
    Dio dio = new Dio();
    ///发送 FormData:
    FormData formData = FormData.fromMap({"name": "张三", "age": 22});
    String url ="http://192.168.200.68:8080/registerUser";
    ///发起 post 请求 如这里的注册用户信息
    Response response = await dio
        .post(url, data: formData);
    result = response.data.toString();
    setState(() {});
  }

抓包所得如下
在这里插入图片描述
我们也可以看到参数的格式
在这里插入图片描述
在这里我们可以看到 Content-type 是 text/plain 而并不是我们上面所说的 multipart/form-data ,这是因为在通过Dio 的 FormData 封装参数时,会进行一步默认的设置如下图所示

在这里插入图片描述

2.2 Dio post 请求提交 json 数据

下面我们使用 dio 发起一个post请求,提交json格式的参数

  ///post请求发送json
  void postRequestFunction2() async{
    String url = "http://192.168.0.102:8080/registerUser2";
    ///创建Dio
    Dio dio = new Dio();
    ///创建Map 封装参数
    Map<String,dynamic> map = Map();
    map['userName']="小明";
    map['userAge']=44;

    ///发起post请求
    Response response =  await dio.post(url,data: map);

    var data = response.data;
  }

抓包所得如下
在这里插入图片描述
从上图中,我们可以看到 Content-Type 标识了传参方式是以 json 格式来发送的,下图中我们可以看到具体的参数
在这里插入图片描述

4 Dio 文件上传并实现进度监听

  ///手机中的图片
  String localImagePath ="/storage/emulated/0/Download/17306285.jpg";
  ///上传的服务器地址
  String netUploadUrl = "http://192.168.0.102:8080/fileupload";

  ///dio 实现文件上传
  void fileUplod() async{
    ///创建Dio
    Dio dio = new Dio();

    Map<String ,dynamic> map = Map();
    map["auth"]="12345";
    map["file"] = await MultipartFile.fromFile(localImagePath,filename: "xxx23.png");
    ///通过FormData
    FormData formData = FormData.fromMap(map);
    ///发送post
    Response response = await dio.post(netUploadUrl, data: formData,
      ///这里是发送请求回调函数
      ///[progress] 当前的进度
      ///[total] 总进度
      onSendProgress: (int progress, int total) {
        print("当前进度是 $progress 总进度是 $total");
      },);
    ///服务器响应结果
    var data = response.data;

  }

通过断点调试
在这里插入图片描述
这里的上传图片请求接口返回了图片的保存路径,我们打开本地服务器的目录
在这里插入图片描述

5 Dio 文件下载并实现进度监听

  ///当前进度进度百分比  当前进度/总进度 从0-1
  double currentProgress =0.0;
  ///下载文件的网络路径
  String apkUrl ="";
  ///使用dio 下载文件
  void downApkFunction() async{
    /// 申请写文件权限
    bool isPermiss =  await checkPermissFunction();
    if(isPermiss) {
      ///手机储存目录
      String savePath = await getPhoneLocalPath();
      String appName = "rk.apk";

      ///创建DIO
      Dio dio = new Dio();

      ///参数一 文件的网络储存URL
      ///参数二 下载的本地目录文件
      ///参数三 下载监听
      Response response = await dio.download(
          apkUrl, "$savePath$appName", onReceiveProgress: (received, total) {
        if (total != -1) {
          ///当前下载的百分比例
          print((received / total * 100).toStringAsFixed(0) + "%");
          // CircularProgressIndicator(value: currentProgress,) 进度 0-1
          currentProgress = received / total;
          setState(() {

          });
        }
      });
    }else{
      ///提示用户请同意权限申请
    }
  }

Android权限目前分为三种:正常权限、危险权限、特殊权限

正常权限 直接在AndroidManifest中配置即可获得的权限。大部分权限都归于此。
危险权限,Android 6.0之后将部分权限定义于此。
危险权限不仅需要需要在AndroidManifest中配置,还需要在使用前check是否真正拥有权限,以动态申请。

在ios中,使用xcode打开本目录

选中Xcode 工程中的 info.plist文件,右键选择Open As - Source Code,将权限配置的代码copy到里面即可,键值对中的内容可按项目需求相应修改。

<!-- 相册 --> 
<key>NSPhotoLibraryUsageDescription</key> 
<string>需要您的同意,APP才能访问相册</string> 
<!-- 相机 --> 
<key>NSCameraUsageDescription</key> 
<string>需要您的同意,APP才能访问相机</string> 
<!-- 麦克风 --> 
<key>NSMicrophoneUsageDescription</key> 
<string>需要您的同意,APP才能访问麦克风</string> 
<!-- 位置 --> 
<key>NSLocationUsageDescription</key> 
<string>需要您的同意, APP才能访问位置</string> 
<!-- 在使用期间访问位置 --> 
<key>NSLocationWhenInUseUsageDescription</key> 
<string>App需要您的同意, APP才能在使用期间访问位置</string> 
<!-- 始终访问位置 --> 
<key>NSLocationAlwaysUsageDescription</key> 
<string>App需要您的同意, APP才能始终访问位置</string> 
<!-- 日历 --> 
<key>NSCalendarsUsageDescription</key> 
<string>App需要您的同意, APP才能访问日历</string> 
<!-- 提醒事项 --> 
<key>NSRemindersUsageDescription</key> 
<string>需要您的同意, APP才能访问提醒事项</string> 
<!-- 运动与健身 --> 
<key>NSMotionUsageDescription</key> 
<string>需要您的同意, APP才能访问运动与健身</string> 
<!-- 健康更新 --> 
<key>NSHealthUpdateUsageDescription</key> 
<string>需要您的同意, APP才能访问健康更新 </string> 
<!-- 健康分享 --> 
<key>NSHealthShareUsageDescription</key> 
<string>需要您的同意, APP才能访问健康分享</string> 
<!-- 蓝牙 --> 
<key>NSBluetoothPeripheralUsageDescription</key> 
<string>需要您的同意, APP才能访问蓝牙</string> 
<!-- 媒体资料库 --> 
<key>NSAppleMusicUsageDescription</key> 
<string>需要您的同意, APP才能访问媒体资料库</string>

在 flutter 项目目录中,我们也可以打开 info.plist 文件配置,如下图所示
在这里插入图片描述
在这里使用的是 permission_handler 插件来申请权限的

permission_handler: ^4.3.0
  • 1

申请权限代码如下

  ///PermissionGroup.storage 对应的是 
  ///android 的外部存储 (External Storage)
  ///ios 的Documents` or `Downloads`
  checkPermissFunction() async {
    if (Theme.of(context).platform == TargetPlatform.android) {
      ///安卓平台中 checkPermissionStatus方法校验是否有储存卡的读写权限 
      PermissionStatus permission = await PermissionHandler()
          .checkPermissionStatus(PermissionGroup.storage);
      if (permission != PermissionStatus.granted) {
        ///无权限那么 调用方法 requestPermissions 申请权限
        Map<PermissionGroup, PermissionStatus> permissions =
            await PermissionHandler()
                .requestPermissions([PermissionGroup.storage]);
        ///校验用户对权限申请的处理
        if (permissions[PermissionGroup.storage] == PermissionStatus.granted) {
          return true;
        }
      } else {
        return true;
      }
    } else {
      return true;
    }
    return false;
  }

申请好权限后,我们需要确定下来储存卡的路径,在这里使用的是 path_provider 插件

path_provider: 1.6.0
  • 1
  ///获取手机的存储目录路径
  ///getExternalStorageDirectory() 获取的是  android 的外部存储 (External Storage)
  ///  getApplicationDocumentsDirectory 获取的是 ios 的Documents` or `Downloads` 目录
  Future<String> getPhoneLocalPath() async {
    final directory = Theme.of(context).platform == TargetPlatform.android
        ? await getExternalStorageDirectory()
        : await getApplicationDocumentsDirectory();
    return directory.path;
  }
6 dio 配制网络代理抓包
  _setupPROXY(Dio dio) {
    (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
        (HttpClient client) {
      client.findProxy = (uri) {
        ///这里的 192.168.0.102:8888就是我们的代理服务地址
        return "PROXY 192.168.0.102:8888";
      };
      client.badCertificateCallback =
          (X509Certificate cert, String host, int port) {
        return true;
      };
    };
  }
7 dio 配制公共请求参数

在实际应用开发中,我们会有像 token、appVersionCode 等等这些每个接口请求都需要传的参数 ,称之为公共请求参数,那么在这里 dio 的请求中我们可以考虑这样来配制

  String application = "V 1.2.2";
  int appVersionCode = 122;
  ///[url]网络请求链接 
  ///[data] post 请求时传的json数据
  ///[queryParameters] get请求时传的参数
  void configCommonPar(url,data,Map<String, dynamic> queryParameters){
    ///配制统一参数
    if (data != null) {
      data['application'] = application;
      data['appVersionCode'] = appVersionCode.toString();
    } else if (queryParameters != null) {
      queryParameters['application'] = application;
      queryParameters['appVersionCode'] = appVersionCode.toString();
    } else {
      ///url中有可能拼接着其他参数
      if (url.contains("?")) {
        url += "&application=$application&appVersionCode=$appVersionCode";
      } else {
        url += "?application=$application&appVersionCode=$appVersionCode";
      }
    }
  }
}

8 dio 配制Content-Type 与请求 header

我们在创建 Dio对象时,会初始化一个 Baseoptions 来创建 Dio

      BaseOptions options = BaseOptions();
      ///请求header的配置
      options.headers["appVersionCode"]=406;
      options.headers["appVersionName"]="V 4.0.6";
      
      options.contentType="application/json";
      options.method="GET";
      options.connectTimeout=30000;
      ///创建 dio
      Dio dio = new Dio(options);

我们也可以在每次发送 get 、post 等不同的请求时,通过 dio 获取到 默认的 options 然后修改一下

void getRequestFunction2() async {
    ///用户id
    int userId = 3;
    ///创建 dio
    Dio dio = new Dio();

    ///请求地址
    ///传参方式1
    String url = "http://192.168.0.102:8080/getUser/$userId";
    ///在这里修改 contentType
    dio.options.contentType="application/json";
    ///请求header的配置
    dio.options.headers["appVersionCode"]=406;
    dio.options.headers["appVersionName"]="V 4.0.6";
    ///发起get请求
    Response response = await dio.get(url);

  ...
  }
 
9 dio 取消网络请求

实际开发中,例如我们退出一个页面时,如果网络请求没完成,就会行成内存泄露,所以需要在页面消毁时,取消网络请求,或者是在下载一个文件时,时间太长了,用户点击取消,就需要取消网络连接

///创建取消标志
CancelToken cancelToken = new CancelToken();
void getRequestFunction2() async {
    ///用户id
    int userId = 3;
    ///创建 dio
    Dio dio = new Dio();
    ///请求地址
    ///传参方式1
    String url = "http://192.168.0.102:8080/getUser/$userId";
    ///发起get请求 并设置 CancelToken 取消标志
    Response response = await dio.get(url,cancelToken: cancelToken);

  ...
  }
 

那么当我们需要手动取消这个网络请求时,只需要调用如下方法

   ///取消网络请求
   if(cancelToken!=null&&!cancelToken.isCancelled){
      cancelToken.cancel();
      cancelToken=null;
    }

需要注意的是,一个 cancelToken 只能对就一个网络请求。


完毕