尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

鸿蒙Flutter网络层实战:dio配置、权限与踩坑指南

鸿蒙Flutter网络层实战:dio配置、权限与踩坑指南 Flutter开发鸿蒙应用聊到网络十个群里九个会问dio怎么配。前面两篇我们把开发环境、工程骨架和基础组件都过了一遍这篇直接进入正题用dio把网络请求跑通并且能应对鉴权、超时、取消、上传下载这些真实场景。内容不光是贴代码我会把鸿蒙上特有的权限配置、明文HTTP限制、证书校验问题一起讲清楚照着敲完你App里的网络层就算立住了。先给结论dio在鸿蒙的Flutter运行时里可以直接用不用为ohos单独写一套原生请求代码。原因是OpenHarmony的Flutter引擎把dart:io的socket和http能力映射到了系统网络栈dio默认的IO适配器能正常工作。但这不代表没有坑——鸿蒙的权限模型、网络安全策略和Android、iOS都不一样很多写好的网络代码一跑到真机上就报奇奇怪怪的错误。这篇指南的核心价值就是把这些坑提前给你填上。如果你已经看过前两篇可以直接照着抄哪怕你是第一次做鸿蒙Flutter开发只要把每一步的原因看懂也能顺顺利利跑通。1. 先搞明白dio在鸿蒙上到底是怎么干活的1.1 不需要为鸿蒙重写一套网络层很多从Android转过来做鸿蒙Flutter开发的朋友第一反应是鸿蒙的网络请求是不是要用自家的ohos.net.http其实不用。Flutter应用跑在鸿蒙上时Dart虚拟机里的dart:io网络层已经由OpenHarmony的Flutter引擎重新实现并映射到系统网络能力上。也就是说你在Dart侧写的HttpClient、Socket、dio这些代码最终会走鸿蒙系统的网络栈但对外API完全一致。这意味着什么意味着你可以把Android和iOS上已经调通的dio封装直接搬到鸿蒙工程里业务层代码一行不用改。我最早就是从现有Flutter项目复制network层代码到鸿蒙工程只改了module.json5和两处安全配置其余全部原样跑通。这个跨端复用能力是Flutter做鸿蒙开发最大的价值——同样一套登录、拉列表、上传图片的代码维护一套就够了不用每个平台各写一份。dio这个库在Dart生态里做网络请求地位基本相当于Android里的OkHttp加Retrofit的组合。它自带拦截器、取消请求、表单上传、文件下载、请求进度这些高频能力配合Flutter自身不提供重量级网络库的现状dio几乎成了Flutter网络层的默认选择。在鸿蒙的Flutter运行时里dio底层走dart:io的adapter所以dio 4.x和5.x在适配版Flutter上都能正常解析、连接、收发数据。1.2 为什么选dio而不是http或原生HttpClient如果你只写一个几行代码的demo用官方轻量级http包确实够用。但一旦进入真实项目很快会遇到几个痛点每个请求都要手动拼HeaderToken过期时每个页面的请求都要处理一遍401上传大文件时UI进度条无从下手用户切走了还不想浪费流量得支持取消。这几个需求http包和Dart自带的HttpClient都做得不够彻底dio却是一等公民。能力diohttpdart:io HttpClient拦截器有支持异步链式处理无无请求取消CancelToken可批量取消无有但使用繁琐上传/下载进度原生回调需手写Stream需手写StreamBaseUrl等全局配置支持需自己封装需自己封装响应自动转换json/formData/bytes需手写需手写社区生态非常活跃问题好搜一般官方底层dio还有一个对我很有吸引力的点它在5.x版本把错误类型统一成了DioException超时、连接失败、证书错误、取消操作都做了清晰的枚举分类配合拦截器可以集中处理。做鸿蒙开发时真机环境千奇百怪比如网络切换导致的连接重置、证书校验失败、明文流量被拦截这类错误分类能让线上问题的定位效率高很多。基本上一个稍微认真一点的Flutter鸿蒙项目第一版网络层就值得直接上dio而不是先用http包再中途迁移。1.3 项目里什么时候需要复杂的网络封装我说一个简单的判断标准。如果你的应用只有两三个接口、只做GET请求、不求长期维护那随便写写都行。但只要出现下面任何一种情况就该把网络层认真封装起来需要登录所有接口都要带Token后端有统一返回结构例如{code, message, data}用户可能断网、弱网、换网络需要超时与重试有文件上传、下载或图片加载的需求。对照一下绝大多数商业项目至少命中两条。所以接下来的封装方案我按一个中等复杂度业务App的标准来做全局Dio实例管配置拦截器管鉴权和统一解包CancelToken管页面级取消这样写到第四个页面时你就知道这些设计有多值钱了。而且这套结构从第一行代码就固定下来后面每个人照着写不会出现一个项目里十种网络请求写法。2. 工程准备三步让dio在鸿蒙真机上跑出第一个请求2.1 确认你的Flutter SDK支持ohos平台先说个很多新手踩过的坑官方Flutter SDK现在还没有ohos这个target必须使用OpenHarmony社区维护的适配版Flutter SDK。判断方法很简单跑一下flutter --version flutter devices如果当前SDK支持鸿蒙flutter devices在连接鸿蒙设备或打开模拟器后会列出对应设备创建工程时也能用到--platforms ohos参数flutter create --platforms ohos .执行完这条命令工程根目录会出现一个ohos/目录里面是HAP工程的结构后续鸿蒙侧的module.json5、资源配置都在这里改。如果你的工程是在旧版本上创建的也可以用同样的命令把ohos平台补进来。这一步没做对后面dio配置得再完美也装不到设备上。这一篇是网络请求专题SDK怎么装、hdc怎么连设备就不展开了但你至少要能通过flutter run把空页面跑上鸿蒙真机或模拟器环境不通谈网络层没有意义。2.2 添加dio依赖在pubspec.yaml的dependencies区域加一行即可dependencies: flutter: sdk: flutter dio: ^5.4.0然后执行flutter pub get为什么建议用5.x而不是4.x因为5.x的DioException命名、Duration类型的超时参数和Dart 3的新语法配合更顺官方也早已停更4.x。如果你是从老项目迁移注意4.x用的DioError在5.x里已经统一改为DioException不替换会导致编译报错。这个迁移坑后面会专门讲。2.3 给鸿蒙应用声明网络权限这是鸿蒙最容易忽略的一步。鸿蒙应用打包成HAP后对系统能力的访问必须显式声明。网络请求需要INTERNET权限打开ohos/entry/src/main/module.json5在module节点下添加requestPermissions{ module: { name: entry, type: entry, srcEntry: src/main/ets/entryability/EntryAbility.ts, requestPermissions: [ { name: ohos.permission.INTERNET } ] } }添加位置一定是module节点的直接子节点不是abilities里面。改完module.json5后需要重新构建HAP才能生效单纯flutter pub get不会帮你处理原生侧配置。如果你在真机上发现请求一直超时但同一个WiFi下电脑上接口秒开十有八九就是权限没加或者加错了位置。提示INTERNET属于normal级别权限不需要用户在运行时授权声明即生效。但如果你后续要读取网络状态做弱网提示还需要额外声明ohos.permission.GET_NETWORK_INFO这里先记住。2.4 跑通第一个GET请求最简场景不用做任何封装先验证环境通不通。下面这段代码我建议你放进一个测试页面里跑import package:dio/dio.dart; final dio Dio(); Futurevoid fetchTest() async { try { final response await dio.get(https://httpbin.org/get); // ignore: avoid_print print(response.data); } on DioException catch (e) { // ignore: avoid_print print(请求失败: ${e.type} - ${e.message}); } }这段代码的逻辑很简单创建一个Dio实例发一个GET请求成功打印响应失败打印DioException的类型和消息。重点在失败分支——不要只打印message打印e.type更容易定位问题。比如它告诉你的是connectionTimeout还是connectionError排查方向完全不同。如果这一步能在鸿蒙真机上打印出响应数据说明dart:io的socket通道在鸿蒙运行时里没问题权限也正确接下来可以放心做架构层的事情。如果失败了对照文末的排查表绝大多数问题都出在网络权限、明文HTTP限制、或者设备本身没连上网络三件事上。3. 脱离demo把dio封装成生产可用的网络层3.1 全局单例与BaseOptions统一配置项目稍微一复杂绝对不要在每个页面new一个Dio。原因有三连接池没法复用性能差每个实例都要重新配一遍BaseOptions容易漏拦截器逻辑散落各处后来的人维护成本极高。正确做法是建一个全局Dio单例集中管理配置。final Dio dio Dio( BaseOptions( baseUrl: https://your-api.example.com, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), sendTimeout: const Duration(seconds: 10), headers: { Content-Type: application/json, }, ), );几个配置项的取值逻辑我说一下。connectTimeout是建立TCP连接的超时一般10秒足够弱网环境再长用户体验也很差receiveTimeout是两次数据包间隔的超时注意不是整个请求的总时长比如下载大文件时这个值可以设大sendTimeout对普通小请求影响不大上传文件时有必要单独设置。如果你有某个请求需要特殊超时可以在这个Dio实例的基础上通过Options临时覆盖不用改全局配置。3.2 拦截器请求头、响应解包、错误处理一网打尽dio的拦截器是它最核心的竞争力几乎就是为生产环境设计的。三个钩子分别处理请求前、响应后、出错后三个阶段。我通常按职责拆成多个拦截器而不是写成一个巨无霸。第一个是请求头拦截器负责在每次请求前把Token塞进Headerclass AuthInterceptor extends InterceptorsWrapper { override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final token TokenManager.getToken(); if (token ! null token.isNotEmpty) { options.headers[Authorization] Bearer $token; } handler.next(options); } }Token从哪来不是这里的关键关键是你要理解handler.next(options)这一步调用next表示放行请求继续往下走如果调用handler.reject(...)请求会被终止并进入错误链路。对Token为空的场景你可以next让后端去拒绝也可以在本地直接reject省一次网络往返。第二个是响应解包拦截器。很多后端返回格式是{code: 0, message: ok, data: {...}}。与其每个页面都判一遍code不如在拦截器里统一处理dio.interceptors.add( InterceptorsWrapper( onResponse: (response, handler) { final body response.data; if (body is Map body[code] ! null body[code] ! 0) { handler.reject( DioException( requestOptions: response.requestOptions, message: body[message] ?? 业务错误, ), ); return; } response.data body is Map ? body[data] : body; handler.next(response); }, onError: (e, handler) { if (e.type DioExceptionType.badResponse) { // 这里可以做401跳登录、弹Toast等操作 } handler.next(e); }, ), );这个拦截器的价值在于业务层拿到的response.data已经是干净的data字段不用每个页面重复判断code而业务错误被转换成了DioException异常链路统一管理。做鸿蒙项目时尤其是团队从别的平台带过来的这种统一规范能极大减少各写各的代码风格。提示如果用handler.reject抛业务错误一定要把response.data带进DioException否则后续排查时看不到实际返回内容。另外onError里最后一个handler.next(e)必须调用否则错误不会传递到调用方的catch块页面会一直卡在await上。3.3 超时、取消和请求去重超时配置前面讲过了这里重点说取消。Flutter页面在返回时如果还在等网络响应不做任何处理的话最常见的问题就是请求回来之后setState一个已经dispose的State直接抛异常。用CancelToken可以优雅解决class SearchPage extends StatefulWidget { override StateSearchPage createState() _SearchPageState(); } class _SearchPageState extends StateSearchPage { final CancelToken _cancelToken CancelToken(); override void dispose() { _cancelToken.cancel(页面销毁取消请求); super.dispose(); } Futurevoid _search(String keyword) async { try { final resp await dio.get( /search, queryParameters: {q: keyword}, cancelToken: _cancelToken, ); // 更新UI } on DioException catch (e) { if (CancelToken.isCancel(e)) { return; // 是主动取消不用处理 } // 真正异常的处理 } } }还有一个高频场景是搜索框输入防抖加取消上一次请求。用户在文本框里连续输入每次都发请求前面的慢响应回来反而覆盖了后面新的结果。我的做法是每次输入变化时先用cancelToken.cancel掉上一次请求再发新请求。旧请求的catch分支通过CancelToken.isCancel判断直接return页面逻辑非常干净。这套组合在鸿蒙上实测稳定推荐直接抄。3.4 文件上传与下载的鸿蒙注意事项上传文件是网络层里比较烦的部分dio的FormData把它简化了不少final formData FormData.fromMap({ description: 封面图, file: await MultipartFile.fromFile(/path/to/your/file.jpg), }); final response await dio.post(/upload, data: formData);这里真正的问题不在dio而在“文件路径从哪来”。鸿蒙的应用沙箱隔离比Android更严格你随便写一个/sdcard/xxx.jpg在真机上大概率拿不到。我踩过的坑是用系统相册或者文件选择器拿到的是一个uri而不是Dart侧能直接打开的文件路径需要先通过鸿蒙提供的文件访问能力把uri转换成沙箱内的可读路径。具体转换方式会随API版本变化但你只要记住“不要硬编码绝对路径”这个大原则就少踩一半的坑。下载文件的代码也很直接await dio.download( https://your-api.example.com/file.zip, /data/storage/el2/base/haps/entry/files/download.zip, onReceiveProgress: (received, total) { // 更新下载进度条 }, );下载路径同样要在应用沙箱目录内。鸿蒙应用获取沙箱路径可以在原生侧通过getContext().filesDir之类的接口拿到再传给Flutter或者用支持鸿蒙的path_provider适配版。下载要比上传多考虑一件事断点续传。dio的download本身不内置断点续传需要结合Range头自己实现。如果你的产品有下载大文件需求这块建议单独开一篇来讲别把下载代码和普通请求堆在一起。上传方面尤其要提醒如果是大文件记得看返回的错误类型是连接超时还是服务端拒绝前者要调大sendTimeout后者往往是后端对文件大小有限制。4. 鸿蒙上最容易踩的网络坑4.1 明文HTTP请求被安全策略拦截这是我在鸿蒙调试时遇到过最迷惑的问题。接口地址是http://电脑浏览器打开正常安卓上跑也正常一到鸿蒙真机就报错日志里写着CLEARTEXT communication to xxx not permitted。原因很简单鸿蒙的网络安全配置默认禁止明文HTTP流量强制要求应用使用HTTPS这是从系统层面提升安全性。开发阶段要验证内网接口可以临时放开明文限制。鸿蒙提供了网络安全配置文件在module.json5的metadata节点里指定networkSecurityConfig指向一个json文件在里面把base-config的cleartext-traffic-permitted设为true。不同API版本的配置字段名略有差异动手前先查一下你当前SDK版本的官方文档。我给你的建议是调通后立刻改回HTTPS。明文HTTP在公网环境下等于裸奔用户数据随便被中间人抓取任何负责任的应用都不该为了开发省事把它留在生产包。如果是因为后端没有HTTPS证书推到生产前把证书配上才是正路。4.2 自签名证书与内网测试环境内网开发经常用自签名HTTPS证书dio默认会验证证书链遇到不受信任的证书直接抛出badCertificate错误。很多人的第一反应是全局关闭证书校验我强烈不建议这么做——这等于把应用的传输层安全全部关掉一旦线上包也带着这个配置后果不堪设想。如果你只是在联调阶段访问内网测试机可以用一个临时的HttpClient适配器只对指定host放行import dart:io; import package:dio/io.dart; (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient () { final client HttpClient(); client.badCertificateCallback (cert, host, port) { return host your.internal.host; }; return client; };这段代码的意思非常明确只有证书校验失败的host是内网测试机时才放行其他host一概严格校验。等到正式环境时把这段配置去掉代码里不留任何后门。如果你连badCertificateCallback都用不惯另一个思路是直接把内网证书安装到鸿蒙设备信任区但这种方式在团队协作时比较难管理不如上面的方案可控。4.3 页面销毁后的setState炸弹Flutter里的经典问题网络请求在返回后调setState但页面已经pop掉了。鸿蒙上有一个更隐蔽的触发场景用户把应用切到后台系统可能对网络连接做一些限制或挂起等恢复前台时请求才回来此时页面状态可能已经重建。我遇到过一次日志报“setState() called after dispose()”排查了半天最后发现是页面销毁时没取消请求。解决方案就是不依赖玄学统一规矩所有页面级请求必须持有自己的CancelToken在dispose()里cancel。不要图省事直接用全局CancelToken因为A页面的取消会把B页面的请求也带崩。再稳一点的方案是在页面里定义一个_mounted标识await返回后先判断再setState双保险。对于鸿蒙这种多任务切换比较频繁的系统这层防护不是可选项是必选项。4.4 弱网、断网和网络切换时的表现鸿蒙设备在WiFi和蜂窝网络之间切换或者从弱网恢复到正常网络时已经建立的TCP连接经常不能复用dio会报SocketException或者connectionError。这时候如果直接给用户弹“网络错误”体验很不好。我建议对幂等请求比如GET列表做一层自动重试dio.interceptors.add( InterceptorsWrapper( onError: (e, handler) async { if (e.type DioExceptionType.connectionError e.requestOptions.extra[retryCount] null) { await Future.delayed(const Duration(milliseconds: 500)); e.requestOptions.extra[retryCount] 1; try { final response await dio.fetch(e.requestOptions); handler.resolve(response); return; } catch (_) {} } handler.next(e); }, ), );重试不是越多越好。我一般只重试一次间隔500毫秒到1秒目的就是应对网络切换瞬间的抖动。POST这种非幂等请求重试前一定要想清楚会不会导致重复下单、重复扣款宁可让用户手动再点一次按钮也不能拿业务安全换体验。在鸿蒙的Flutter开发里这个“网络切换重试”的场景确实比Android更容易触发值得在架构期就预留好。5. 问题排查速查与我的调试习惯5.1 常见错误类型对照表把dio的DioException类型和实际含义做成了一张速查表遇到问题先看它属于哪一类再决定怎么处理。DioException类型常见原因处理思路connectionTimeoutTCP连接建立超时检查网络权限、设备联网状态适当调大connectTimeoutsendTimeout数据发送超时常见于上传大文件调大sendTimeout或检查服务端接收能力receiveTimeout响应间隔超时服务端响应太慢对于下载任务可单独调大connectionError连接被重置、网络切换、socket异常检查网络稳定性幂等请求可做一次重试badCertificate证书过期、自签名、证书链不完整测试环境按host放行生产环境必须修复证书badResponse服务端返回4xx/5xx打印statusCode和响应体和后端联调cancel主动取消或页面销毁取消用CancelToken.isCancel判断静默处理unknown其他未知错误打全日志先复现再定位这张表看着简单但解决了我一堆实际问题。以前遇到网络错误都是瞎试不是改后端就是重启设备。后来改成先判断e.type再行动效率高了几倍。你可以把它贴在项目Wiki里团队新人排查问题时直接按图索骥。5.2 定位网络问题的标准步骤我排查鸿蒙Flutter网络问题的固定流程按顺序执行基本没有漏过问题的先用设备自带浏览器或电脑在同一网络下访问目标接口确认服务端本身没挂打开dio的LogInterceptor或者在onError里打印e.type和完整message看日志区分是DNS解析失败、TCP连接失败、TLS握手失败还是不合理的HTTP响应对应检查DNS问题查域名配置TCP问题查网络权限和防火墙TLS问题查证书HTTP响应问题查后端逻辑如果权限和网络都正常再检查是不是鸿蒙的网络安全策略拦了明文HTTP。第2步是关键。很多人上来就翻业务代码其实网络层错误最值钱的信息都在DioException类型和底层message里。我在鸿蒙上遇到过好几次“上传失败”的报错点开message才看到原因是sendTimeout根本不是业务问题是接口在大文件面前太慢了。这个习惯帮我省了无数次跟后端吵架的机会。5.3 两个让我少走很多弯路的小习惯最后分享两个我从项目里沉淀下来的习惯。第一个debug模式必开LogInterceptor。在Dio实例上加上dio.interceptors.add(LogInterceptor(responseBody: true))上线前再在构建配置里关掉。开发阶段它能帮你看到完整的请求URL、Header、响应体和耗时遇到问题根本不需要瞎猜。鸿蒙的日志输出位置和Android不太一样但LogInterceptor在Flutter侧输出DevTools和命令行都能看到。第二个封装一个统一的请求入口函数而不是在页面里直接调用dio.get。比如叫apiGetT(String path, {MapString, dynamic? query})统一处理loading、成功、失败三种状态页面里代码量最少出错的概率也最低。自己封装的时候记住一件事不要在函数里循环调DioException打印日志日志会打得很乱把关键信息返回给页面页面决定怎么展示。这两个习惯都不复杂但它们把网络层的使用规范固定在了代码结构里团队里新手来了照着写就行这也算是我在被无数个混乱的请求代码折磨之后总结出来的教训。
返回列表