zero_network_kit
English | 简体中文
A Flutter plugin for network diagnostics: connectivity inspection, latency probing, DNS resolution, port checks, bandwidth measurement, quality scoring and micro-benchmarks — for Android, iOS, macOS, Windows, Linux and Web (partial).
🔔 Upgrade recommended:
1.1.0fixes the macOS ICMP ping timeout unit, HTTP client leaks, iOS VPN false positives and DNS latency/encoding bugs, makes macOS return native network details, and documents exactly which native fields each platform fills. Upgrade to^1.1.0.
🌐 Official Website · 📦 View on pub.dev · 🔗 View on GitHub
Table of Contents
- Features
- Getting started
- Usage
- Testing your own code against it
- Platform support
- Documentation
- License
Features
| Capability | API | Notes |
|---|---|---|
| Connectivity | NetworkDiagnostic.checkConnection() |
Transport, IPv4/IPv6, gateway, SSID, RSSI, MAC, VPN — field availability varies by platform |
| Connectivity stream | NetworkDiagnostic.onConnectivityChanged |
Emits a fresh snapshot on every change |
| Latency | NetworkDiagnostic.ping() |
TCP handshake RTT everywhere; system ICMP on desktop |
| DNS | NetworkDiagnostic.resolve() |
system resolver + raw UDP against explicit servers |
| Bandwidth | NetworkDiagnostic.runSpeedTest() |
Download/upload throughput with progress callbacks |
| Ports | NetworkDiagnostic.checkPort() / scanPorts() |
Bounded-concurrency TCP reachability |
| Quality | NetworkDiagnostic.evaluateQuality() |
Weighted 0–100 score + level + suggestions |
| Full report | NetworkDiagnostic.diagnose() |
One-shot aggregate of every probe |
| Capabilities | NetworkDiagnostic.capabilities |
Query-then-call; wifiDetails is Android-only, nativeDetails is mobile-only |
| Benchmarks | NetworkBenchmark.runAll() |
Measures how fast the diagnostics API itself runs |
Design goals:
- Hermetic unit tests — every service accepts injected collaborators
(
ConnectivityAdapter,http.Client,PingService, …), so the whole test suite runs without touching the network. - No hidden state — results are immutable value objects with
toMap(). - Graceful degradation — a missing permission or an unreachable sub-service
never throws through the public API; the corresponding field stays
null.
Getting started
dependencies:
zero_network_kit: ^1.1.0
Android permissions
The plugin manifest already declares INTERNET, ACCESS_NETWORK_STATE and
ACCESS_WIFI_STATE — everything the connectivity, ping, DNS, port and speed
probes need.
Reading the Wi-Fi SSID / BSSID is different: it needs a dangerous-level
permission, so the plugin deliberately does not declare it (a library
<uses-permission> is merged into every host app, which would force the
permission — and the Google Play data-safety declaration — onto integrators that
never read an SSID). Opt in from your host app
(android/app/src/main/AndroidManifest.xml) instead:
<!-- Android 13+: NEARBY_WIFI_DEVICES replaces the location permission.
neverForLocation asserts it is not used to derive a location.
Older devices ignore the unknown permission. -->
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation" />
<!-- Android 8.1 – 12 still needs the location permission. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="32" />
Request the grant at runtime as well (Android 6+). Without it
getNetworkDetails() does not throw — it simply leaves ssid / bssid
empty, per the graceful-degradation contract.
iOS
No configuration is required. Note what the iOS native layer actually returns:
| Field | iOS | Why |
|---|---|---|
ipAddress, ipv6Address |
✅ | read via getifaddrs on en0 |
gateway |
✅ | read from the sysctl routing table (same source as netstat -rn) |
isVpn |
✅ | a tunnel interface carrying a routable IPv4 address |
ssid, bssid, signalStrength |
❌ always null |
needs the Access WiFi Information capability plus location authorisation; the plugin does not request them |
macAddress |
❌ always null |
iOS has returned a constant value since iOS 7 |
Query NetworkDiagnostic.capabilities before rendering: wifiDetails is
Android-only, nativeDetails covers IP / IPv6 / VPN.
Usage
The snippets below cover the common paths. For every method, parameter, default and result field, see the API guide.
import 'package:zero_network_kit/zero_network_kit.dart';
void main() {
// Optional: apply global defaults once.
ZeroNetworkKit.init(
config: const NetworkDiagnosticConfig(
pingHost: '1.1.1.1',
dnsServers: <String>['1.1.1.1', '8.8.8.8'],
),
);
runApp(const MyApp());
}
Connectivity
final info = await NetworkDiagnostic.checkConnection(probeReachability: true);
print('${info.type.label} · ${info.ipAddress} · ${info.signalStrength} dBm');
print('gateway=${info.gateway} vpn=${info.isVpn} reachable=${info.isReachable}');
await for (final change in NetworkDiagnostic.onConnectivityChanged) {
print('now on ${change.type.id}');
}
Just need connectivity? You don't need
init()or any of the other APIs —checkConnection()andonConnectivityChangedwork out of the box. See the Connectivity-only: minimal example in the Connectivity chapter of API.md.
Ping
final result = await NetworkDiagnostic.ping(host: '1.1.1.1', count: 5);
print('${result.received}/${result.sent} replies, '
'loss ${result.packetLoss.toStringAsFixed(1)}%, '
'avg ${result.averageTime.toStringAsFixed(1)} ms, '
'jitter ${result.jitter.toStringAsFixed(2)} ms');
On desktop you can switch to the system ICMP binary:
await NetworkDiagnostic.ping(host: '1.1.1.1', mode: PingMode.icmp);
DNS
final results = await NetworkDiagnostic.resolve(
domain: 'example.com',
dnsServers: const <String>['1.1.1.1', '8.8.8.8'],
includeSystemResolver: true,
);
for (final r in results) {
print('${r.server}: ${r.isSuccess ? r.resolvedIps.join(", ") : r.errorMessage}'
' (${r.responseTimeMs.toStringAsFixed(1)} ms)');
}
Speed test
final speed = await NetworkDiagnostic.runSpeedTest(
onProgress: (progress) => print(
'${progress.phase.name}: ${progress.speedMbps.toStringAsFixed(1)} Mbps',
),
);
print('↓ ${speed.downloadSpeed.toStringAsFixed(2)} Mbps '
'↑ ${speed.uploadSpeed.toStringAsFixed(2)} Mbps');
Download and upload phases write a meaningful amount of traffic. Defaults point
at the public Cloudflare speed endpoints; override downloadUrl / uploadUrl
to use your own.
Ports
if (await NetworkDiagnostic.isPortOpen(host: 'example.com', port: 443)) {
print('HTTPS reachable');
}
final scan = await NetworkDiagnostic.scanPorts(
host: 'example.com',
ports: const <int>[22, 80, 443, 8080],
concurrency: 8,
);
Quality score
final score = await NetworkDiagnostic.evaluateQuality(includeSpeedTest: false);
print('${score.score}/100 (${score.level.label})');
for (final suggestion in score.suggestions) {
print('• $suggestion');
}
The score is a weighted average of the metrics that are available:
| Metric | Weight | Source |
|---|---|---|
latency |
0.25 | PingResult.averageTime |
jitter |
0.10 | PingResult.jitter |
packetLoss |
0.15 | PingResult.packetLoss |
download |
0.25 | SpeedTestResult.downloadSpeed |
upload |
0.15 | SpeedTestResult.uploadSpeed |
dns |
0.10 | mean successful DnsTestResult.responseTimeMs |
signalStrength |
0.10 | NetworkConnectionInfo.signalStrength |
Tune the ideal values with NetworkDiagnosticConfig(qualityTargets: ...).
Full report
final report = await NetworkDiagnostic.diagnose(includePorts: true);
print(report); // NetworkDiagnosticReport(type: wifi, connected: true, score: 87.5)
print(report.toMap()); // JSON encodable
Benchmarks
final suite = await NetworkBenchmark.runAll(iterations: 20, warmupIterations: 3);
for (final result in suite.results) {
print('${result.testName}: avg '
'${(result.averageDuration.inMicroseconds / 1000).toStringAsFixed(2)} ms, '
'${result.operationsPerSecond.toStringAsFixed(1)} ops/s');
}
Benchmarks answer questions like "how expensive is one checkConnection() call
on this device?" — useful when deciding whether a diagnostic belongs on the
startup path.
Testing your own code against it
Every service is injectable, so you can unit-test your integration points without a network:
class FakeConnectivityAdapter implements ConnectivityAdapter {
@override
Future<List<String>> checkConnectivity() async => <String>['wifi'];
@override
Stream<List<String>> get onConnectivityChanged => const Stream.empty();
}
NetworkDiagnostic.configure(
connectivity: ConnectivityService(adapter: FakeConnectivityAdapter()),
);
NetworkDiagnostic.reset() restores the built-in defaults.
Platform support
| Platform | Status |
|---|---|
| Android | ✅ Supported (Kotlin native side) |
| iOS | ✅ Supported (Swift native side) |
| macOS | ✅ Supported (Swift native side) |
| Windows | ✅ Supported (C++ native side) |
| Linux | ✅ Supported (C++ native side) |
| Web | ⚠️ Partial — see Web support below |
Native detail availability
Which fields of NetworkConnectionInfo are actually populated, per platform.
Anything not listed is null; a missing permission never throws.
| Field | Android | iOS | macOS | Windows | Linux | Web |
|---|---|---|---|---|---|---|
ipAddress / ipv6Address |
✅ | ✅ | ✅ | ✅ | ✅ Dart fallback | ❌ |
isVpn |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
gateway |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
macAddress |
✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
ssid / bssid |
✅ needs grant | ❌ | ❌ | ❌ | ❌ | ❌ |
signalStrength |
✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
gateway is read from the sysctl routing table on Android, iOS, macOS and
Windows — it needs no permission or entitlement, so it is available wherever the
native layer is implemented (Linux and Web are the exceptions).
NetworkDiagnostic.capabilities mirrors this as two flags: wifiDetails
(Android only) and nativeDetails (mobile only).
Web support
The web build exposes the same static API. Capabilities that the browser sandbox
forbids degrade gracefully — they return null or an "unavailable" result
instead of throwing:
| Capability | Web | Notes |
|---|---|---|
| Connectivity | ✅ | via the browser's navigator.onLine |
Ping (PingMode.tcp) |
⚠️ | HTTPS round trip; the target must send CORS headers |
Ping (PingMode.icmp) |
❌ | throws UnsupportedError |
| DNS (system resolver) | ✅ | via DNS-over-HTTPS |
| DNS (explicit server) | ⚠️ | needs a DoH endpoint, otherwise "unsupported" |
| Speed test | ✅ | HTTP download / upload |
| Quality score | ✅ | pure function |
| Benchmark | ✅ | pure function |
| Port check / scan | ❌ | returns "unavailable" results |
| Native details (SSID, gateway, MAC, VPN) | ❌ | null |
ZeroNetworkKit.getNativeNetworkDetails() returns null on the web and
ZeroNetworkKit.getPlatformVersion() returns Web. Call
NetworkCapabilities.current() to discover the supported set at runtime.
Documentation
- API.md — complete usage guide: every public API with copy-paste examples, parameter defaults, model reference and common pitfalls.
- AGENTS.md — engineering contract: architecture, conventions and the new-feature checklist.
- CONTRIBUTING.md — how to set up and submit changes.
- CHANGELOG.md — release history.
License
MPL-2.0 © Zero Labs
Third-party dependency, trademark and endorsement notices live in NOTICE.
Libraries
- advanced
- 进阶 / 注入 / 底层 API / Advanced, injectable and low-level APIs.
- zero_network_kit
- Flutter 网络诊断插件 / A Flutter plugin for network diagnostics.
- zero_network_kit_method_channel
- zero_network_kit_platform_interface