字
字节笔记本
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 初始化、证书配置、拦截器 | DioUtil、HttpConfig |
| 中间层 | 请求与 Model 的桥接、统一错误处理 | DioModelControl、BaseResp |
| 业务层 | UI 层调用、状态管理 | State 中的具体方法 |
这种三层架构的优势在于:配置层和中间层高度可复用,新增接口只需在中间层添加方法,业务层保持简洁。由于 Dart 的可选参数特性,在不改变已有代码的基础上可以灵活扩展功能。
分享: