☰
Flutter网络检测最佳实践:从connectivity_plus到Socket主动探测
2026/10/15 8:04:29 网站建设 项目流程

1. 先弄清楚:你检测的到底是“有网”还是“有链路”

做 Flutter 应用的人,大概率都遇到过类似的反馈:“我 WiFi 明明是满格的,为什么 App 一直提示没有网络?”第一次被用户这么问时,我以为是玄学,后来查了一下午才发现,手机的系统状态、插件返回值、以及用户嘴里说的“能上网”,在技术上其实是三个完全不同的概念。如果你只盯着“是否有 WiFi 信号”来判断是否已连接到互联网,那这个判断从一开始就是错的。这篇文章会把 Flutter 场景下检测网络状态这件事从头到尾讲透,包括原理、插件选型、完整代码、参数配置和排障清单,适合正在做跨平台 App、或者在网络模块上反复被坑的 Flutter 开发者。

1.1 系统告诉你“已连接”,并不是真的“已连接”

先做一个简单的实验:关掉路由器的 WAN 口拨号,但把手机继续连在路由器的 WiFi 上。这时候手机右上角依然是 WiFi 图标,系统设置里也显示“已连接”,但打开任何 App 都会转圈圈。对用户来说,这就是“断网”;对系统来说,却是“网络正常”。问题出在哪里?因为手机和路由器之间只有一条物理链路,而互联网在链路的另一端根本没有通。

Flutter 社区常用的 connectivity_plus 插件,返回的 ConnectivityResult.wifi、ConnectivityResult.mobile、ConnectivityResult.ethernet、ConnectivityResult.none,本质上描述的就是这个物理链路。它告诉你的只是“系统当前启用了哪张网卡、连上了哪个热点或基站”,完全不关心链路对面能不能真的发出一个请求、收到一个响应。类似的,iOS 上的网络路径监控框架拿到的结果,反映的也是接口层面的可达性,而不是外网可达性。把这两个概念混为一谈,是绝大多数网络检测 bug 的总根源。

1.2 判定“已连接到互联网”的三个层级

在动手写代码之前,我建议先把“网络状态”拆成三个层级,心里有个坐标,后面所有方案都围绕这个坐标展开。

层级判定内容典型表现说明
L1系统网络接口存在状态栏有信号图标飞行模式关闭、WiFi 开启,但可能还没关联到任何热点
L2接口拿到 IP、网关、DNSWiFi 显示“已连接”已经关联到 AP,但路由器可能没拨号、没分配可用外网 IP
L3能成功发起对外请求并得到响应App 能正常拉取数据这才是用户感知的“有网”

对大多数业务来说,L1、L2 都是过程量,L3 才是终态。比如你要做一个只有在线才能下单的页面,用户在机场连上了需要网页认证的 WiFi,这时候系统显示 L2,浏览器却打不开任何网页,如果你在 App 里只判断到 L2 就放行,后面所有请求都会失败,然后用户就会产生“这是个半成品”的第一印象。反过来,如果你在 L1 阶段就提示“无网”,用户明明看着信号满格,也会觉得你 App 有问题。真正稳妥的检测,必须把判定推进到 L3。

1.3 检测需求拆解:启动时、切换时、操作前

在选型之前,还要想清楚一个事:你到底是“什么时候需要知道网络状态”?不同时机,对方案的延迟和成本要求完全不一样。

  • 启动时:App 冷启动进入首页,这时候只需要一次快速判定,告诉 UI 显示“加载中”还是“没网”。
  • 切换时:从 WiFi 切到蜂窝、从蜂窝回到 WiFi、飞行模式开关,需要马上重新判定,避免状态残留。
  • 操作前:用户点击“提交订单”这类关键动作,可以用一次性检测兜底,防止提交后失败。

这三类场景对应的代码形态分别是:Future 一次性调用、Stream 持续监听、以及 Future 再次调用并配合超时。后面第 3 部分的模块会把三者全部覆盖到。这里先说结论:不要只依赖插件的回调做“永久判断”,一定要把“接口状态变化”当作“重新发起探测”的触发器,而不是把插件的结果当作最终答案。只要有这个意识,后面一半的坑你已经提前绕开了。

2. 工具选型:光靠插件不够,得自己补一个探测层

2.1 connectivity_plus 的能力边界

很多刚接触 Flutter 的开发者,看到 connectivity_plus 里有 checkConnectivity(),又有个 onConnectivityChanged 回调,就以为网络检测解决了。实际上这个插件只解决了两件事:一是拿到当前“正在使用的网络接口类型”,二是在“接口类型发生变化”时通知你。它内部在 Android 上走的是系统的连接管理服务,在 iOS 上走的是网络路径监控,这些都是系统级能力,本身没有问题。

问题在于,系统级能力的设计目标不是“判断互联网是否可达”,而是“判断设备有没有一个正在工作的网络接口”。这个差异在日常使用中会被掩盖,因为大多数时候“有接口”和“能上网”是同时成立的。但一旦遇到热点未拨号、运营商出口故障、WiFi 需要网页认证、DNS 配置异常这类边缘情况,两者就会分道扬镳。所以我的结论是:connectivity_plus 可以要,但只能把它当“信号”,不能把它当“结果”。它告诉你“网络可能变了”,至于到底通不通,得你自己去验证。

2.2 三种补充探测方案对比

既然要补一个探测层,常见做法有三种,我直接列一张对比表,把优缺点摊开。

方案原理优点缺点推荐度
HTTP 请求探测用 http 包请求一个稳定页面或接口,判断是否返回预期状态码最贴近真实业务,结果可靠耗时相对长,依赖 DNS 和证书,可能被鉴权拦截业务关键场景可用
TCP Socket 探测与一个稳定主机建立 TCP 连接,能完成握手就算通耗时短、轻量、不受 HTTP 层约束只能证明 TCP 可达,不能证明业务接口可用日常判断首推
ICMP Ping 探测发送 ICMP 报文,看是否有回包网络工程师习惯用,定位直观移动端系统对 ICMP 限制多,部分路由器防火墙直接丢弃,误判率偏高不推荐

我最终选择 TCP Socket 探测作为主方案,原因是它快、稳、可预期。一次 TCP 握手在正常网络下通常几十毫秒到几百毫秒完成,超时控制在 3 秒左右,比走完整 HTTP 请求要快不少,也不会被某个页面的内容改动、接口限流、证书过期之类的业务因素干扰。如果你要探测的是一个后端业务接口,那 HTTP 方案更合适,因为它验证的其实是“业务链路是否通畅”,而不是“公网是否通畅”,两者要分开理解。

2.3 探测主机和端口怎么选

Socket 探测的核心是“连哪里”。这个点很多人不重视,随手写一个 IP 就上线,结果在不同网络环境下误判率忽高忽低。我的建议是遵循三条原则。

第一条,用公共基础设施而不依赖单个域名。可以选 8.8.8.8、1.1.1.1 这类通用公共地址,也可以选 example.com 这种永久保留域名,关键是这个目标不能是某个小公司的临时服务器,否则对方一挂,你的全量用户都会“断网”。第二条,端口优先选 53 或 443。53 是 DNS 服务端口,几乎不会被主动屏蔽;443 是 HTTPS 默认端口,绝大多数网络都放行。第三条,至少准备两个端点,主端点失败后自动换下一个,避免单个目标抖动造成误判。

我实际项目里的端点列表大概是这样的:一组 IP 直连地址加端口 53,一组域名地址加端口 443,两端都失败才判定为无网。这样做的好处是,即使某个网络把 ICMP 和特定端口都挡住了,只要它允许公网 HTTPS 流量,443 那一路就能成功;而 IP 直连那一路又能绕开 DNS 解析本身出问题的情况。两条腿走路,比单点探测可靠得多。

2.4 为什么不直接用一个现成的探测包

社区里其实有现成的探测包,比如 internet_connection_checker,它把“主动探测”这件事封装好了,初始化时传入一个地址列表,调用方法就能返回布尔值。对于只想快速解决“App 老是误判断网”的团队,直接用现成包是可以的,代码量少,看起来也清晰。

但我在自己的项目里还是选择了自建模块,原因有三个。第一,端点不可控。现成包默认的探测地址是写死的,一旦某个地址在你覆盖的区域表现不佳,你改起来很麻烦,甚至要等社区更新。第二,内部逻辑是黑盒。它对超时、重试、缓存的处理方式不一定符合你的业务节奏,出了误判问题你只能猜。第三,依赖体积和维护成本。为一个几十行的探测逻辑引入一个包,还要跟着它的版本升级走,性价比并不高。如果你要的是精细控制、动态配置、和埋点上报,自建一个几十行的模块反而更省心。

3. 完整实现:一个生产可用的网络探测模块

3.1 模块的整体设计

现在进入正题。我先说设计,再给代码。整个模块我拆成三层:探测层(真正发 Socket 请求)、判定层(合并接口状态与探测结果)、通知层(把结果通过 Stream 和一次性 Future 暴露给外部)。

用一个类 ConnectivityService 串起来,内部维护三个关键成员:connectivity_plus 的实例、一个提供给外部订阅的流控制器、以及上一次探测结果的缓存。为什么要缓存?因为网络状态检测是一个被高频触发的事件,WiFi 和蜂窝切换的瞬间,系统回调可能连续触发好几次,如果没有缓存和节流,UI 会被无意义地重建,甚至出现“有网-无网-有网”来回跳的闪烁。

缓存这里我建议不要做得太复杂,一个布尔字段加一个时间戳就够了。比如探测成功后在 15 秒内再次查询,直接返回缓存结果,不重复发 Socket;超过 15 秒则重新探测。这个窗口期要根据业务调整,电商类 App 可以短一点,内容类 App 可以长一点。窗口太短会放大并发探测的问题,太长又会让结果过于陈旧,15 秒是我试下来比较均衡的默认值。

3.2 核心探测:Socket 握手的正确写法

先给核心的探测函数。这里用的是 dart:io 自带的 Socket,不需要引入额外依赖。

import 'dart:async'; import 'dart:io'; class ConnectivityService { ConnectivityService({this.probeHosts = defaultProbeHosts}); static const List<ProbeTarget> defaultProbeHosts = [ ProbeTarget('8.8.8.8', 53), ProbeTarget('example.com', 443), ]; final List<ProbeTarget> probeHosts; Future<bool> probeInternet({Duration timeout = const Duration(seconds: 3)}) async { for (final target in probeHosts) { final ok = await _probeOnce(target, timeout); if (ok) return true; } return false; } Future<bool> _probeOnce(ProbeTarget target, Duration timeout) async { Socket? socket; try { socket = await Socket.connect(target.host, target.port, timeout: timeout); return socket != null; } catch (_) { return false; } finally { socket?.destroy(); } } } class ProbeTarget { const ProbeTarget(this.host, this.port); final String host; final int port; }

这段代码的要点有三个。第一个是超时必须显式传,Socket.connect 在部分平台上如果不传 timeout,可能会长时间阻塞,用户都切换到飞行模式了,你还傻等十几秒。第二个是成功后要立刻 destroy,Socket 连接建立只是为了确认握手,不需要保持,不释放会造成文件描述符泄漏。第三个是循环探测时,一个失败立刻换下一个,而不是把超时时间累加,三个端点都试完的总耗时仍然控制在 3 秒左右。

顺带说一句,为什么我要把 probeHosts 作为构造函数参数传进去?因为不同地区的网络对目标地址的连通性差异很大。你自己维护一个白名单端点、或者从后端配置下发,比写死在代码里灵活得多。后面第 5 部分我会专门说端点的管理。

3.3 监听网络变化并自动重测

有了基础探测函数,接下来把它接到系统的网络变化事件上。connectivity_plus 的 onConnectivityChanged 是一个流,每个事件代表接口状态变了,我们在收到事件后重新探测一次,再把结果发给业务方。

StreamSubscription<List<ConnectivityResult>>? _subscription; final _statusController = StreamController<bool>.broadcast(); bool _cached = true; DateTime _lastChecked = DateTime.fromMillisecondsSinceEpoch(0); Stream<bool> get statusStream => _statusController.stream; void startMonitoring() { _subscription ??= connectivity.onConnectivityChanged.listen((results) { _handleConnectivityChanged(results); }); } Future<void> _handleConnectivityChanged(List<ConnectivityResult> results) async { final hasInterface = results.isNotEmpty && !results.contains(ConnectivityResult.none); if (!hasInterface) { _publishResult(false); return; } // 接口刚从无到有,网络栈可能还没就绪,稍微等一等 await Future<void>.delayed(const Duration(milliseconds: 400)); final reachable = await probeInternet(); _publishResult(reachable); } void _publishResult(bool reachable) { _cached = reachable; _lastChecked = DateTime.now(); if (!_statusController.isClosed) { _statusController.add(reachable); } }

这里有两个细节非常值得注意。第一,connectivity_plus 新版本的回调参数是 List ,因为在部分系统上可能同时存在多个可用接口,判空时要先检查列表是否为空、再检查是否包含 none,顺序写反会漏判。第二,接口从“无”变“有”之后,不要立刻探测,我在代码里特意延迟了 400 毫秒。原因是 WiFi 关联成功后,系统可能还在拿 IP、配置网关,这个窗口期去连 Socket,大概率超时,于是你会把一次正常连接误判成“断网”,之后得等下一次事件触发才能恢复,体验非常差。

排查过这类问题的人应该懂,这种“连接成功后头几百毫秒内探测失败”是线上问题重灾区,不是你代码写错了,是时序没给够。加一个小延迟是最省事、也最稳妥的办法。

3.4 一次性检查接口与 UI 层结合

持续监听适合全局提示“网络已断开”,但很多业务场景只需要在某个时刻查一次。比如用户点“重试加载”按钮时,你希望同步返回“当前到底有没有网”。这两个需求我都暴露出去。

Future<bool> get hasConnection async { if (DateTime.now().difference(_lastChecked) < const Duration(seconds: 15)) { return _cached; } final result = await connectivity.checkConnectivity(); final hasInterface = result.isNotEmpty && !result.contains(ConnectivityResult.none); if (!hasInterface) { _publishResult(false); return false; } final reachable = await probeInternet(); _publishResult(reachable); return reachable; } void dispose() { _subscription?.cancel(); _statusController.close(); }

UI 层用起来就非常简单了。全局网络状态提示可以这样写:

StreamBuilder<bool>( stream: service.statusStream, initialData: true, builder: (context, snapshot) { return snapshot.data == false ? const OfflineBanner() : const SizedBox.shrink(); }, )

关键操作前的兜底检查则可以写成:

Future<void> submitOrder() async { if (!await service.hasConnection) { showToast('当前无网络,请检查连接后再试'); return; } // 继续提交逻辑 }

到这里,一个最小可用的网络探测模块就齐了。注意,我说的是“最小可用”,如果项目要求更严谨,第 4 部分的坑你必须提前看完,否则上线后大概率会收到“明明有网却提示无网”之类的新问题。

3.5 接入状态管理:以 Provider 为例

很多项目不是直接把类 new 出来用,而是挂在状态管理框架里。以 Provider 为例,可以把这个服务注册成单例,同时用一个 ChangeNotifier 包一层,让页面可以直接监听。

class ConnectivityNotifier extends ChangeNotifier { ConnectivityNotifier(this._service) { _sub = _service.statusStream.listen((value) { _online = value; notifyListeners(); }); _service.startMonitoring(); } final ConnectivityService _service; StreamSubscription<bool>? _sub; bool _online = true; bool get online => _online; Future<void> refresh() async { _online = await _service.hasConnection; notifyListeners(); } @override void dispose() { _sub?.cancel(); _service.dispose(); super.dispose(); } }

这样页面里通过 context.read 拿到 notifier,直接读 online 字段就行;需要手动刷新时调用 refresh。整个模块的职责边界很清楚:探测逻辑在 ConnectivityService,UI 绑定在 notifier,业务代码只面向 bool 和回调,不碰任何 Socket 细节。后续如果换探测策略,改动也被限制在一个类内部,不会波及整个项目。

4. 常见问题与排查技巧实录

4.1 权限没配就开跑,Android 上会先翻车

Android 上如果 Manifest 里没有声明网络权限,Socket.connect 会直接抛出异常,而且这个异常在 debug 模式下不一定显眼,容易被当成一般错误忽略。正确配置是这两行:

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

第一行管 Socket 和 HTTP,第二行管 connectivity_plus 读取网络状态,缺一不可。另外提醒一句:如果你的 App 只检测 WiFi,不要只写 ACCESS_WIFI_STATE 而漏了 ACCESS_NETWORK_STATE,后者覆盖面更广,前者只适用于读取热点列表这类场景。

iOS 这边,Socket 探测本身不需要额外权限,系统没有“使用 Socket 必须先申请”的规则。如果你用的是 HTTP 探测,并且目标地址是明文 http,那就要看 App Transport Security 的设置了,默认不允许明文流量,需要在 Info.plist 里配置例外规则。不过我更推荐直接走 https,一是避免 ATS 的麻烦,二是防止某些公共 WiFi 对明文流量做拦截。

4.2 WiFi 满格却上不了网:那 400 毫秒之外的真相

这种问题通常分三类,排查的时候按顺序过一遍。第一类,路由器本身没拨号成功,手机能拿到内网 IP,但所有外网请求都出不去,表现为 Socket 超时。代码层面能做的就是如实上报“无网”,同时在提示语里加一句“检查路由器或宽带状态”,别让用户以为你 App 坏了。第二类,WiFi 是开放的公共热点,连接后需要网页认证才能上网,用户没打开浏览器完成认证,系统却已经显示“已连接”。这类情况最棘手,因为 TCP 探测有时能通,业务请求却全被重定向到认证页。

对于第二类,我试过比较有效的方法是:探测时不要停在 TCP 握手层,再多走一步,发一个 HTTP HEAD 请求到一个固定的探测地址,检查返回的状态码。如果返回 200 或者重定向到已知域名,说明正常;如果返回的是某个陌生页面的 200,说明很可能被强制门户劫持了。这时候 App 可以提示用户“当前 WiFi 需要认证,请在浏览器中完成登录”,用户完成认证后回来自动重测。这个方案比单纯 TCP 探测更接近真实用户体验,代价是多一点延迟,但值得。

第三类,DNS 配置异常。偶尔会遇到路由器分配的 DNS 解析不出来,表现为域名永远超时,但 IP 直连正常。这也是为什么我在探测端点里坚持保留至少一个 IP 直连目标。如果你的日志里出现“IP 探测成功但域名探测失败”的组合,基本就能锁定是 DNS 的问题。

4.3 网络切换瞬间的状态闪烁

WiFi 和蜂窝切换、飞行模式开关的瞬间,系统的事件回调可能在一两秒内连续触发多次,现象是顶部离线提示条出现又消失,或者页面上的网络状态一会儿一变。这种问题不是探测逻辑错了,是没做防抖。

处理思路是给“无网”状态加一个确认窗口。我的做法是:收到 none 事件时,不立刻广播 false,而是先等 800 毫秒左右,如果这期间没有新的“有接口”事件回来,再确认断网;如果马上又收到有接口事件,就重新走探测流程。这里要注意,防抖窗口和前面说的 400 毫秒就绪延迟是两个东西,一个是为了避免误报,一个是为了避免探测过早失败,别混在一起。

4.4 后台运行和锁屏后,监听就失灵了

很多开发者把网络监听放在全局单例里,以为 App 退到后台还能正常收到回调。实际情况是,系统在省电模式下会挂起 App 的网络进程和事件分发,connectivity_plus 的流在后台可能延迟很久才推一次,甚至直接不推。等用户切回前台,界面上还停留在几十分钟前的状态。

我的经验是:不要指望后台监听,而是在 App 生命周期回到前台时,主动调用一次 hasConnection 强制刷新。Flutter 里可以用生命周期组件的回调感知前后台切换,回到前台后触发一次完整探测。这样既省电,又能保证用户看到的第一眼状态是可信的。另外,如果你需要在后台保持网络长连接做业务,那就不是这个模块的范畴了,该考虑的是系统级推送通道或者常驻机制,方向完全不同。

4.5 超时时间拍脑袋写,误判率翻倍

超时时间是最容易被忽视的参数。给太短,比如 800 毫秒,在弱网环境下几乎必触发,用户本来在地铁里用蜂窝网络,你硬说人家“断网”;给太长,比如 10 秒,用户断网后要等 10 秒才看到离线提示,体验同样糟糕。

我踩过几次坑之后固定下来的方案是分档超时:探测阶段用 3 秒,这是 Socket 握手比较合理的等待上限;HTTP 阶段用 5 秒,因为要算上 DNS 解析和 TLS 握手。如果你面向的地区网络质量偏差,可以把 Socket 超时放宽到 4 秒,但不要超过 5 秒,超过之后误判率下降,但用户等待时间明显上升,性价比很低。还有一个小技巧:把每次探测的实际耗时记录下来,线上日志里统计平均耗时和长尾分布,你会发现不同地区差异很大,再用数据反推参数,比拍脑袋准得多。

4.6 模拟器上一切正常,真机却各种翻车

Android 模拟器默认走宿主机的网络,你在电脑上能上网,模拟器里大概率也能过探测;iOS 模拟器直接共享宿主机的网络栈,表现更接近真机。所以模拟器只能验证逻辑,不能验证网络栈差异。真机上一个常见差异是,部分设备在系统层对后台 Socket 做了限制,即使你的 App 在前台,一些深度休眠模式下 Socket 也可能被延迟建立,表现为偶发超时。遇到这种情况,先看日志里的探测耗时,如果分布在 2.5 秒到 3 秒边缘,说明触碰到了超时阈值,可以考虑把探测端点换成延迟更低的本地公共基础设施,而不是盲目调大超时。

另外提醒一句,真机调试时可以把系统开发者选项里的模拟弱网模式打开,再跑一遍探测流程,能提前复现很多用户场景,比上线后被反馈强多了。弱网下 Socket 握手偶尔失败是正常现象,只要你最终的判定逻辑没有把单次失败直接升级成“永久无网”,问题就不大。

5. 工程化落地:放进生产环境前,再做四件事

5.1 探测端点可配置,不要写死在代码深处

模块上线一段时间后,你一定会遇到需要更换端点的情况。可能是某个公共目标在你覆盖的地区不稳定,也可能是想换成更贴近业务区域的探测地址。如果端点写死在代码里,每次调整都要发版,非常被动。

我的建议是:默认配置保留一份内置端点列表,同时预留一个从远程配置读取覆盖值的通道。远程配置更新后,模块在下次探测时自动使用新列表。这里要注意一个边界条件:如果远程配置为空或者列表全不可达,要能回退到内置列表,否则后端配置下发失败会导致全量用户“无网”。另外,端点列表不要包含太多项,三到四个足够,多了只是徒增探测耗时。

5.2 加一层短时缓存,别让每个页面都触发一次探测

一个页面三五个组件同时监听网络状态,每个都在初始化阶段调用 hasConnection,如果不做缓存,一次网络抖动就能打出几十个 Socket 请求,浪费电量和流量。我在模块里做的缓存规则是:15 秒内重复查询直接返回上一次结果;超过 15 秒才重新探测。同时订阅事件触发的探测结果也会写入缓存,这样业务方拿到的结果始终是“最近的、有效的”。如果你的业务对时效性要求高,把窗口缩到 5 秒也可以,但别直接取消缓存,否则并发请求重复探测的问题马上会回来。

5.3 把“探测通过”和“业务请求成功”分开看待

很多团队把网络探测模块当成业务请求的前置校验,探测失败就拦住请求。这个思路方向没错,但要注意,探测成功不等于业务请求一定成功。用户此时可能处于某个受限网络里,TCP 握手能通,但业务接口的域名解析异常,或者接口所在端口被限制。所以更稳妥的做法是:业务请求失败时,先区分错误类型,超时、连接被拒绝、证书错误分别处理,再结合网络探测模块的结果,决定是提示“无网”、提示“服务器开小差”还是弹出重试。不要把所有失败都归给网络,这样排查问题时会少走很多弯路。

5.4 埋点与日志:让“无网”问题可被追溯

最后一项是我强烈建议加的。把每次探测的结果、耗时、端点、触发来源(启动、网络切换、手动查询)都打一条日志并埋点上报。线上用户抱怨“没网”时,如果没有这些数据,你只能靠猜;有了数据,你可以直接查这批用户当时的探测耗时是否接近超时、哪个端点失败了、是接口切换触发的还是启动时就失败,很多问题马上就能定位。

我见过一个真实案例:某个版本更新后,大量用户反馈“首页一直提示没有网络”。埋点数据显示,他们的 Socket 探测全部超时,但日志里又显示手机确实有 WiFi 信号。后来定位到是更新包把探测目标地址写错了,解析出来的是内网地址,所有用户自然都“无网”。这种问题如果没有埋点,光靠用户描述,几天都不一定能查出来。

5.5 Web 与桌面端的行为差异

如果你的 Flutter 工程同时支持 Web 和桌面端,网络检测要单独看待。Web 端浏览器的网络策略和移动端差异很大,connectivity_plus 在 Web 上能获取到的信息非常有限,很多时候拿不到具体的接口类型。Socket 连接在 Web 端也不能直接用 dart:io,需要走浏览器提供的网络能力,比如通过一个资源请求来间接判断。桌面端相对好一些,dart:io 的 Socket 可用,但桌面系统对网络接口的管理更复杂,虚拟网卡、多网卡并存时,接口状态可能频繁变化,防抖和缓存的节奏要重新调。

我的处理方式是:工程内按平台抽象一个网络探测接口,移动端走 Socket 加 HTTP 的组合,Web 端退化为 HTTP 资源探测,桌面端沿用移动端逻辑但把防抖窗口拉大。这样做虽然代码多一点,但避免了“移动端调试好好的,一跑 Web 全断网”的尴尬。

最后分享一个小习惯:每次改完探测逻辑,我都在真机上把飞行模式开关、WiFi 开关、热点认证这三件事轮流做一遍,确认状态响应能在一两秒内收敛。做过一轮之后,心里就有底了。你按第 3 部分的代码起步,把第 4 部分的坑对着过一遍,再按第 5 部分补上配置、缓存和埋点,这套网络检测模块基本就能扛住绝大多数线上场景。我自己在实际项目里最深的体会是:网络检测的难点从来不在“能不能写出探测代码”,而在于愿不愿意去处理那些系统状态和真实体验不一致的边界情况,把这些边界一条条理清楚,比多写一百行业务代码更有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询