字节笔记本

2026年3月22日

Flutter 网络请求封装:基于 Dio 的三层架构方案

这篇文章将介绍 Flutter 中基于 Dio 的三层网络请求封装方案,包括配置启动层、中间层和业务层的完整实现,帮助开发者构建可复用、易维护的网络模块。

导包

pubspec.yaml 文件中加入依赖:

yaml
dependencies:
  flutter:
    sdk: flutter
  dio: ^2.1.13

配置层

基础 Bean Model

一般网络回调数据都是类似下面这种格式:

dart
class BaseResp<T> {
  String status;
  int code;
  String message;
  T data;

  BaseResp(this.status, this.code, this.message, this.data);

  @override
  String toString() {
    StringBuffer sb = new StringBuffer('{');
    sb.write("\"status\":\"$status\"");
    sb.write(",\"code\":$code");
    sb.write(",\"message\":\"$message\"");
    sb.write(",\"data\":\"$data\"");
    sb.write('}');
    return sb.toString();
  }
}

泛型为你的基础数据,可能是 List 也可能是其他类型,后面的中间层会使用到。

还有一种类型,后台返回的是流,可以使用:

dart
class BaseRespR<T> {
  String status;
  int code;
  String message;
  T data;
  Response response;

  BaseRespR(this.status, this.code, this.message, this.data, this.response);

  @override
  String toString() {
    StringBuffer sb = new StringBuffer('{');
    sb.write("\"status\":\"$status\"");
    sb.write(",\"code\":$code");
    sb.write(",\"message\":\"$message\"");
    sb.write(",\"data\":\"$data\"");
    sb.write('}');
    return sb.toString();
  }
}

response 为 Dio 中的类。

URL 路径工具

接口的不同路径配置,不包括 baseUrl:

dart
class ApiUrls {
  // 登陆
  static const String LOGIN = "xxxxx/sss/xxx";
  // 获取设备列表
  static const String GETDEVICELIST = "xxxxx/xxxx";
  // 获取绑定列表
  static const String GETDEVICEBINDLIST = "xxxx/xxxxxx";

  // baseUrl
  static const BASEURL = "http://www.example.com/api/v1/";

  static String getPath({String path: ''}) {
    StringBuffer sb = new StringBuffer(path);
    return sb.toString();
  }
}

请求执行层

dart
/// 请求方法
class Method {
  static final String get = "GET";
  static final String post = "POST";
  static final String put = "PUT";
  static final String head = "HEAD";
  static final String delete = "DELETE";
  static final String patch = "PATCH";
}

/// Http 配置
class HttpConfig {
  HttpConfig({
    this.status,
    this.code,
    this.msg,
    this.data,
    this.options,
    this.pem,
    this.pKCSPath,
    this.pKCSPwd,
  });

  /// BaseResp [String status]字段 key, 默认: status
  String status;
  /// BaseResp [int code]字段 key, 默认: code
  String code;
  /// BaseResp [String msg]字段 key, 默认: message
  String msg;
  /// BaseResp [T data]字段 key, 默认: data
  String data;
  /// Options
  BaseOptions options;
  /// PEM 证书内容
  String pem;
  /// PKCS12 证书路径
  String pKCSPath;
  /// PKCS12 证书密码
  String pKCSPwd;
}

/// 单例 DioUtil
class DioUtil {
  static final DioUtil _singleton = DioUtil._init();
  static Dio _dio;

  String _statusKey = "status";
  String _codeKey = "code";
  String _msgKey = "message";
  String _dataKey = "data";
  BaseOptions _options = getDefOptions();
  String _pem;
  String _pKCSPath;
  String _pKCSPwd;
  static bool _isDebug = false;

  static DioUtil getInstance() => _singleton;
  factory DioUtil() => _singleton;

  DioUtil._init() {
    _dio = new Dio(_options);
    _dio.interceptors.add(LogInterceptor(responseBody: false));
  }

  /// 打开 debug 模式
  static void openDebug() {
    _isDebug = true;
  }

  void setCookie(String cookie) {
    Map<String, dynamic> _headers = new Map();
    _headers["Cookie"] = cookie;
    _dio.options.headers.addAll(_headers);
  }

  /// set Config
  void setConfig(HttpConfig config) {
    _statusKey = config.status ?? _statusKey;
    _codeKey = config.code ?? _codeKey;
    _msgKey = config.msg ?? _msgKey;
    _dataKey = config.data ?? _dataKey;
    _mergeOption(config.options);
    _pem = config.pem ?? _pem;
    if (_dio != null) {
      _dio.options = _options;
      if (_pem != null) {
        (_dio.httpClientAdapter as DefaultHttpClientAdapter)
            .onHttpClientCreate = (client) {
          client.badCertificateCallback = (X509Certificate cert, String host, int port) {
            if (cert.pem == _pem) return true;
            return false;
          };
        };
      }
    }
  }

  /// 发起网络请求
  Future<BaseResp<T>> request<T>(String method, String path,
      {data, Options options, CancelToken cancelToken,
      Map<String, dynamic> queryParameters, String pathReplace, String match}) async {

    if (match != null && pathReplace != null) {
      path = path.replaceAll(match, pathReplace);
    }

    Response response = await _dio.request(path,
        data: data,
        queryParameters: queryParameters,
        options: _checkOptions(method, options),
        cancelToken: cancelToken);
    _printHttpLog(response);

    String _status;
    int _code;
    String _msg;
    T _data;

    if (response.statusCode == HttpStatus.ok ||
        response.statusCode == HttpStatus.created) {
      try {
        if (response.data is Map) {
          _status = (response.data[_statusKey] is int)
              ? response.data[_statusKey].toString()
              : response.data[_statusKey];
          _code = (response.data[_codeKey] is String)
              ? int.tryParse(response.data[_codeKey])
              : response.data[_codeKey];
          _msg = response.data[_msgKey];
          _data = response.data[_dataKey];
        } else {
          Map<String, dynamic> _dataMap = _decodeData(response);
          _status = (_dataMap[_statusKey] is int)
              ? _dataMap[_statusKey].toString()
              : _dataMap[_statusKey];
          _code = (_dataMap[_codeKey] is String)
              ? int.tryParse(_dataMap[_codeKey])
              : _dataMap[_codeKey];
          _msg = _dataMap[_msgKey];
          _data = _dataMap[_dataKey];
        }
        return new BaseResp(_status, _code, _msg, _data);
      } catch (e) {
        return new Future.error(new DioError(
          response: response,
          message: "data parsing exception...",
          type: DioErrorType.RESPONSE,
        ));
      }
    }
    return new Future.error(new DioError(
      response: response,
      message: "statusCode: $response.statusCode, service error",
      type: DioErrorType.RESPONSE,
    ));
  }

  /// 返回带 Response 的请求
  Future<BaseRespR<T>> requestR<T>(String method, String path,
      {data, Options options, CancelToken cancelToken,
      Map<String, dynamic> queryParameters}) async {
    Response response = await _dio.request(path,
        data: data,
        queryParameters: queryParameters,
        options: _checkOptions(method, options),
        cancelToken: cancelToken);
    _printHttpLog(response);

    String _status;
    int _code;
    String _msg;
    T _data;

    if (response.statusCode == HttpStatus.ok ||
        response.statusCode == HttpStatus.created) {
      try {
        if (response.data is Map) {
          _status = (response.data[_statusKey] is int)
              ? response.data[_statusKey].toString()
              : response.data[_statusKey];
          _code = (response.data[_codeKey] is String)
              ? int.tryParse(response.data[_codeKey])
              : response.data[_codeKey];
          _msg = response.data[_msgKey];
          _data = response.data[_dataKey];
        } else {
          Map<String, dynamic> _dataMap = _decodeData(response);
          _status = (_dataMap[_statusKey] is int)
              ? _dataMap[_statusKey].toString()
              : _dataMap[_statusKey];
          _code = (_dataMap[_codeKey] is String)
              ? int.tryParse(_dataMap[_codeKey])
              : _dataMap[_codeKey];
          _msg = _dataMap[_msgKey];
          _data = _dataMap[_dataKey];
        }
        return new BaseRespR(_status, _code, _msg, _data, response);
      } catch (e) {
        return new Future.error(new DioError(
          response: response,
          message: "data parsing exception...",
          type: DioErrorType.RESPONSE,
        ));
      }
    }
    return new Future.error(new DioError(
      response: response,
      message: "statusCode: $response.statusCode, service error",
      type: DioErrorType.RESPONSE,
    ));
  }

  /// 下载文件
  Future<Response> download(String urlPath, savePath,
      {ProgressCallback onProgress, CancelToken cancelToken,
      data, Options options}) {
    return _dio.download(urlPath, savePath,
        onReceiveProgress: onProgress,
        cancelToken: cancelToken,
        data: data,
        options: options);
  }

  /// 上传文件
  Future<BaseResp<T>> upload<T>(String method, String path,
      {data, Options options, CancelToken cancelToken,
      Map<String, dynamic> queryParameters}) async {
    Response response = await _dio.post(path, data: data,
        options: _checkOptions(method, options),
        cancelToken: cancelToken,
        queryParameters: queryParameters);

    String _status;
    int _code;
    String _msg;
    T _data;

    if (response.statusCode == HttpStatus.ok ||
        response.statusCode == HttpStatus.created) {
      try {
        if (response.data is Map) {
          _status = (response.data[_statusKey] is int)
              ? response.data[_statusKey].toString()
              : response.data[_statusKey];
          _code = (response.data[_codeKey] is String)
              ? int.tryParse(response.data[_codeKey])
              : response.data[_codeKey];
          _msg = response.data[_msgKey];
          _data = response.data[_dataKey];
        } else {
          Map<String, dynamic> _dataMap = _decodeData(response);
          _status = (_dataMap[_statusKey] is int)
              ? _dataMap[_statusKey].toString()
              : _dataMap[_statusKey];
          _code = (_dataMap[_codeKey] is String)
              ? int.tryParse(_dataMap[_codeKey])
              : _dataMap[_codeKey];
          _msg = _dataMap[_msgKey];
          _data = _dataMap[_dataKey];
        }
        return new BaseResp(_status, _code, _msg, _data);
      } catch (e) {
        return new Future.error(new DioError(
          response: response,
          message: "data parsing exception...",
          type: DioErrorType.RESPONSE,
        ));
      }
    }
    return new Future.error(new DioError(
      response: response,
      message: "statusCode: $response.statusCode, service error",
      type: DioErrorType.RESPONSE,
    ));
  }

  /// 解码响应数据
  Map<String, dynamic> _decodeData(Response response) {
    if (response == null ||
        response.data == null ||
        response.data.toString().isEmpty) {
      return new Map();
    }
    return json.decode(response.data.toString());
  }

  /// 检查 Options
  Options _checkOptions(method, options) {
    if (options == null) {
      return new Options(
          method: method,
          responseType: ResponseType.json,
          contentType: ContentType.parse("application/json; charset=utf-8"));
    }
    return options;
  }

  /// 合并 Option
  void _mergeOption(BaseOptions opt) {
    _options.method = opt.method ?? _options.method;
    _options.headers = (new Map.from(_options.headers))..addAll(opt.headers);
    _options.baseUrl = opt.baseUrl ?? _options.baseUrl;
    _options.connectTimeout = opt.connectTimeout ?? _options.connectTimeout;
    _options.receiveTimeout = opt.receiveTimeout ?? _options.receiveTimeout;
    _options.responseType = opt.responseType ?? _options.responseType;
    _options.extra = (new Map.from(_options.extra))..addAll(opt.extra);
    _options.contentType = opt.contentType ?? _options.contentType;
  }

  /// 打印 Http 日志
  void _printHttpLog(Response response) {
    if (!_isDebug) return;
    try {
      print("---------------Http Log---------------" +
          "\n[statusCode]:   " + response.statusCode.toString() +
          "\n[request   ]:   " + _getOptionsStr(response.request));
      _printDataStr("reqdata ", response.request.data);
      _printDataStr("response", response.data);
    } catch (ex) {
      print("Http Log error......");
    }
  }

  String _getOptionsStr(RequestOptions request) {
    return "method: " + request.method +
        "  baseUrl: " + request.baseUrl +
        "  path: " + request.path;
  }

  void _printDataStr(String tag, Object value) {
    String da = value.toString();
    while (da.isNotEmpty) {
      if (da.length > 512) {
        print("[$tag  ]:   " + da.substring(0, 512));
        da = da.substring(512, da.length);
      } else {
        print("[$tag  ]:   " + da);
        da = "";
      }
    }
  }

  static BaseOptions getDefOptions() {
    BaseOptions options = new BaseOptions();
    options.contentType = ContentType.parse("application/json; charset=utf-8");
    options.connectTimeout = 10000 * 30;
    options.receiveTimeout = 10000 * 30;
    return options;
  }
}

支持 GET、POST、PUT、HEAD、DELETE、PATCH 等操作,我们用中间层将 Model 与请求执行层耦合起来。

中间层

数据 Model

首先生成数据模型的 Bean,比如用户信息 UserBean

dart
class UserBean {
  String value;
  int timestamp;
  int userId;

  UserBean({this.value, this.timestamp, this.userId});

  UserBean.fromJson(Map<String, dynamic> json) {
    value = json['value'];
    timestamp = json['timestamp'];
    userId = json['userId'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['value'] = this.value;
    data['timestamp'] = this.timestamp;
    data['userId'] = this.userId;
    return data;
  }
}

推荐工具:json_to_dart 可以将 JSON 数据直接转换为 Dart Model 类。

网络请求封装

中间层将 DioUtil 的底层请求与具体的业务 Model 结合:

dart
class DioModelControl {
  DioModelControl();
  DioModelControl _dioModelControl;

  DioModelControl.getInstans() {
    if (null == _dioModelControl) {
      _dioModelControl = new DioModelControl();
    }
  }

  /// 登陆
  Future<UserBean> login(String loginLabel, String password,
      {Function printError, BuildContext context}) async {
    BaseResp<Map<String, dynamic>> baseResp = await DioUtil()
        .request<Map<String, dynamic>>(
            Method.post, ApiUrls.getPath(path: ApiUrls.LOGIN),
            data: {"loginLabel": loginLabel, "password": password});
    UserBean userBean;

    if (baseResp.code == 200 || baseResp.code == 201 || baseResp.code == 204) {
      if (baseResp.data != null) {
        userBean = UserBean.fromJson(baseResp.data);
      }
      return userBean;
    }
    return _error<UserBean>(baseResp, printError: printError, context: context);
  }

  /// 获取父设备列表
  Future<DeviceListBean> getDeviceList(String roomNo, String orgName,
      String devId, int pageNumber,
      {String parentDevId = "000000000000",
      Function printError, BuildContext context}) async {
    BaseResp<Map<String, dynamic>> baseResp = await DioUtil()
        .request<Map<String, dynamic>>(
            Method.post, ApiUrls.getPath(path: ApiUrls.GETDEVICELIST),
            data: {"roomNo": roomNo, "orgName": orgName, "devId": devId, "parentDevId": parentDevId},
            queryParameters: {"pageNumber": pageNumber});
    DeviceListBean deviceListBean;
    if (baseResp.code == 200 || baseResp.code == 201 || baseResp.code == 204) {
      if (baseResp.data != null) {
        deviceListBean = DeviceListBean.fromJson(baseResp.data);
      }
      return deviceListBean;
    }
    return _error<DeviceListBean>(baseResp, printError: printError, context: context);
  }

  /// 根据名字获取机构
  Future<List<MechanismBean>> getMechanismListBeanWithName(String name,
      {Function printError, BuildContext context}) async {
    BaseResp<List> baseResp = await DioUtil().request<List>(
        Method.get,
        ApiUrls.getPath(path: ApiUrls.GETMECHANISLISTBEANWITHNAME),
        queryParameters: {"name": name});
    List<MechanismBean> list;
    if (baseResp.code == 200 || baseResp.code == 201 || baseResp.code == 204) {
      if (baseResp.data != null) {
        list = baseResp.data.map((value) {
          return MechanismBean.fromJson(value);
        }).toList();
      }
      return list;
    }
    return _error<List<MechanismBean>>(baseResp, printError: printError, context: context);
  }

  /// 删除设备
  Future<String> deleteDevice(int id,
      {Function printError, BuildContext context}) async {
    BaseResp baseResp = await DioUtil().request(
        Method.delete, ApiUrls.getPath(path: ApiUrls.DELETEDEVICE),
        match: "{id}", pathReplace: "$id");
    String message;
    if (baseResp.code == 200 || baseResp.code == 201 || baseResp.code == 204) {
      if (baseResp.message != null) {
        message = baseResp.message;
      }
      return message;
    }
    return _error<String>(baseResp, printError: printError, context: context);
  }

  /// 上传文件
  Future<UploadBean> uploadFile(String directory, String mediaType,
      UploadFileInfo upfile,
      {Function printError, BuildContext context}) async {
    FormData formData = new FormData.from({"file": upfile});
    BaseResp<Map<String, dynamic>> baseResp = await DioUtil()
        .request<Map<String, dynamic>>(
            Method.post, ApiUrls.getPath(path: ApiUrls.UPLOADFILE),
            data: formData,
            queryParameters: {"directory": directory, "mediaType": mediaType});
    UploadBean uploadBean;
    if (baseResp.code == 200 || baseResp.code == 201 || baseResp.code == 204) {
      if (baseResp.data != null) {
        uploadBean = UploadBean.fromJson(baseResp.data);
      }
      return uploadBean;
    }
    return _error<UploadBean>(baseResp, printError: printError, context: context);
  }

  /// 统一错误处理
  Future _error<T>(BaseResp baseResp,
      {Function printError, BuildContext context}) {
    if (baseResp.code == 401 && context != null) {
      RouteUtil.goLogin(context);
      return new Future<T>.error(baseResp.message);
    }
    showToast(baseResp.message);
    if (printError != null) {
      printError(baseResp.message);
      return new Future<T>.error(baseResp.message);
    } else {
      if (baseResp.code == 401) {
        return new Future<T>.error("tokenerror");
      }
      return new Future<T>.error(baseResp.message);
    }
  }
}

注意:query 参数用 queryParameters,body 参数用 data,path 参数替换参考删除设备的实现,文件上传使用 FormData。

业务层

业务层是在 State 中具体使用网络请求的层:

dart
/// 上传文件
void _sendUpload() {
  UploadFileInfo upfile = UploadFileInfo(
      new File(filePath), filePath.split('/').last);
  PopuUtils.showLoading(context, "正在上传");
  DioModelControl.getInstans()
      .uploadFile("assistant", widget.type == 1 ? "voice" : "video", upfile)
      .then((value) {
    ctMusic.text = value.urlPrefix + value.url;
    widget.sceneResponseList[0]?.url = value.urlPrefix + value.url;
    Navigator.of(context).pop();
    RouteUtil.popAndBackResult(context, widget.sceneResponseList);
  });
}

/// 登陆
void _login() {
  DioModelControl.getInstans()
      .login(_unameController.value.text, _pwdController.value.text,
          printError: (value) {
    showToast(value);
  }).then((value) {
    if (!ObjectUtil.isEmptyString(value.value)) {
      SpUtil.putString(BaseConfig.keyAppToken, value.value);
    }
  }).then((value) {
    RouteUtil.goMain(context);
  });
}

/// 刷新列表
void _onRefresh() {
  _page = 1;
  _list.clear();
  DioModelControl.getInstans()
      .getDeviceList(_room_no, _org_name, _mac_str, _page, context: context)
      .then((value) {
    if (value != null && value.items != null) {
      _list.addAll(value.items);
      if (mounted) setState(() {});
      _refreshController.refreshCompleted();
    }
  });
}

架构总结

整个网络封装分为三层:

层级职责核心类
配置层Dio 初始化、证书配置、拦截器DioUtilHttpConfig
中间层请求与 Model 的桥接、统一错误处理DioModelControlBaseResp
业务层UI 层调用、状态管理State 中的具体方法

这种三层架构的优势在于:配置层和中间层高度可复用,新增接口只需在中间层添加方法,业务层保持简洁。由于 Dart 的可选参数特性,在不改变已有代码的基础上可以灵活扩展功能。

分享: