netstatus
Unified network + real internet status as a single Dart Stream. Multi-platform (Android, iOS, Web, Desktop), configurable ping, retry/timeout, and connection type. Pure Dart Streams with advanced mitigations for production use.
Key Features
- Single Stream API:
Stream<NetStatus>
with values:noNetwork
,networkOnly
,internet
- Connection Classification:
NetType
detection (wifi, mobile, ethernet, vpn, unknown) - Multi-Platform: Android, iOS, Web, Desktop via
connectivity_plus
+ HTTP/DNS checks - Production Ready: Configurable timeouts, retries, backoff, throttling, and fallbacks
- Web Optimized: CORS-aware with image-ping fallback, multiple endpoint support
- Energy Efficient: Smart throttling, trailing recheck, optional periodic validation
- Developer Friendly: Logging hooks, error capture, immediate status access
Why netstatus?
Unlike basic connectivity checkers, netstatus distinguishes between network presence and actual internet access:
noNetwork
: No network interface availablenetworkOnly
: Connected to network but no internet (captive portal, WAN down, etc.)internet
: Full internet connectivity confirmed via HTTP/DNS
Perfect for apps that need reliable connectivity detection across all platforms.
Quick Start
Add to pubspec.yaml
:
dependencies:
netstatus: ^0.1.2
Basic usage:
import 'package:netstatus/netstatus.dart';
final service = NetStatusService();
// Listen to status changes
service.observeNetStatus().listen((status) {
switch (status) {
case NetStatus.noNetwork:
print('No network connection');
case NetStatus.networkOnly:
print('Connected but no internet');
case NetStatus.internet:
print('Full internet access');
}
});
// Get current status immediately
final current = await service.checkNow();
final connectionType = await service.getCurrentNetType();
// Clean up
service.dispose();
Advanced Configuration
final service = NetStatusService(
NetCheckConfig(
// Multiple endpoints for reliability
pingUrls: const [
Uri.parse('https://clients3.google.com/generate_204'),
Uri.parse('https://cloudflare.com/cdn-cgi/trace'),
],
// Strict 2xx responses only (avoid captive portal false positives)
expectedStatusLowerBound: 200,
expectedStatusUpperBound: 299,
// Energy efficient settings
minIntervalBetweenChecks: const Duration(seconds: 10),
retry: 2,
retryBackoffMultiplier: 2.0,
// Advanced features
enableDnsFallback: true,
dnsSuccessIsInternet: false, // Keep strict
enablePeriodicRecheck: true,
periodicInterval: const Duration(minutes: 1),
// Debugging
onLog: (message) => debugPrint('[NetStatus] $message'),
onError: (error, stack) => debugPrint('NetStatus Error: $error'),
),
);
// Stream with immediate last value for new listeners
service.observeNetStatusWithInitial().listen((status) {
// Receives last known status immediately, then updates
});
Configuration Options
Option | Type | Default | Description |
---|---|---|---|
pingUrl |
Uri | Google 204 | Single endpoint to check |
pingUrls |
List | null | Multiple endpoints (tries in order) |
pingMethod |
String | 'HEAD' | HTTP method ('HEAD' or 'GET') |
timeout |
Duration | 2s | Request timeout |
retry |
int | 1 | Number of retries |
retryDelay |
Duration | 500ms | Base delay between retries |
retryBackoffMultiplier |
double? | null | Exponential backoff (e.g., 2.0) |
minIntervalBetweenChecks |
Duration | 5s | Throttle interval |
allowMobile |
bool | true | Allow internet checks on mobile |
expectedStatusLowerBound |
int | 200 | Min HTTP status for success |
expectedStatusUpperBound |
int | 299 | Max HTTP status for success |
expectedBodyContains |
String? | null | Required substring in response |
maxBodyBytes |
int | 2048 | Max bytes to read when checking body |
emitNetworkOnlyBeforePing |
bool | true | Quick UI feedback |
enablePeriodicRecheck |
bool | false | Background validation |
periodicInterval |
Duration | 30s | How often to recheck |
enableDnsFallback |
bool | false | Try DNS if HTTP fails |
dnsHosts |
List | '1.1.1.1', 'dns.google' |
DNS hosts to resolve |
dnsSuccessIsInternet |
bool | false | Whether DNS success = internet |
trailingRecheck |
bool | true | Recheck after throttle period |
onLog |
Function? | null | Logging callback |
onError |
Function? | null | Error callback |
Platform-Specific Notes
Web
- CORS may block cross-origin requests
- Image-ping fallback automatically attempted on CORS failures
- No DNS fallback available (browser limitation)
- Configure
pingUrls
to CORS-enabled endpoints or your own domain
Android
Required permissions in android/app/src/main/AndroidManifest.xml
:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Optional for enhanced detection:
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
iOS/Desktop
No special configuration required. Uses standard network APIs.
Web CORS Solutions
Express (Node.js)
// server.js
const express = require("express");
const app = express();
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") return res.sendStatus(204);
next();
});
app.head("/ping", (req, res) => res.sendStatus(204));
app.get("/ping", (req, res) => res.sendStatus(204));
const port = process.env.PORT || 3000;
app.listen(port, () => console.log("ping on :" + port));
Cloudflare Worker
export default {
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/ping") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,OPTIONS",
},
});
}
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,OPTIONS",
},
});
}
return new Response("ok", { status: 200 });
},
};
Then set in Flutter:
final service = NetStatusService(NetCheckConfig(
pingUrl: Uri.parse('https://your-domain.example/ping'),
pingMethod: 'HEAD',
));
Production Best Practices
Energy & Data Optimization
NetCheckConfig(
minIntervalBetweenChecks: const Duration(minutes: 1), // Reduce frequency
retry: 1, // Minimize failed attempts
enablePeriodicRecheck: false, // Disable background checks
retryBackoffMultiplier: 2.0, // Exponential backoff
)
Reliability & Accuracy
NetCheckConfig(
pingUrls: [
Uri.parse('https://your-api.com/health'), // Your own endpoint
Uri.parse('https://cloudflare.com/cdn-cgi/trace'),
],
expectedBodyContains: 'ok', // Validate response content
enableDnsFallback: true,
dnsSuccessIsInternet: false, // Keep strict validation
)
Development & Debugging
NetCheckConfig(
onLog: (msg) => debugPrint('[NetStatus] $msg'),
onError: (error, stack) => logger.error('NetStatus', error, stack),
minIntervalBetweenChecks: const Duration(seconds: 2), // Faster feedback
)
Enterprise/Privacy Focused
NetCheckConfig(
pingUrls: [
Uri.parse('https://internal-health.company.com/ping'),
],
enableDnsFallback: false, // Avoid external DNS queries
allowMobile: false, // WiFi only for cost control
)
Common Patterns
React to Connectivity Changes
StreamSubscription? _netSub;
@override
void initState() {
super.initState();
_netSub = netService.observeNetStatus().listen((status) {
switch (status) {
case NetStatus.noNetwork:
showSnackBar('No internet connection');
case NetStatus.networkOnly:
showSnackBar('Limited connectivity - some features unavailable');
case NetStatus.internet:
hideConnectivityWarnings();
}
});
}
@override
void dispose() {
_netSub?.cancel();
netService.dispose();
super.dispose();
}
Conditional API Calls
Future<void> syncData() async {
final status = await netService.checkNow();
if (status == NetStatus.internet) {
await api.uploadPendingData();
await api.downloadUpdates();
} else if (status == NetStatus.networkOnly) {
// Maybe try cached/local operations
showRetryOption();
} else {
showOfflineMessage();
}
}
Background Monitoring
final service = NetStatusService(
NetCheckConfig(
enablePeriodicRecheck: true,
periodicInterval: const Duration(minutes: 5),
onLog: (msg) => debugPrint(msg),
),
);
// Automatic background validation - useful for long-running apps
Troubleshooting
Q: Getting networkOnly
but internet works in browser?
A: Likely a captive portal or the ping endpoint is blocked. Try:
- Set
pingUrls
to a different endpoint you control - Enable
onLog
to see what's failing - Check if
expectedBodyContains
is too strict
Q: High battery/data usage? A: Tune throttling and retries:
- Increase
minIntervalBetweenChecks
- Reduce
retry
count - Disable
enablePeriodicRecheck
if not needed - Set
retryBackoffMultiplier: 2.0
Q: Web CORS errors? A: Use your own endpoint or known CORS-friendly URLs:
pingUrls: [
Uri.parse('https://httpbin.org/status/204'), // CORS-enabled
Uri.parse('https://your-domain.com/ping'),
]
Q: Inconsistent NetType
detection?
A: This is limited by the underlying OS/browser APIs. connectivity_plus
does its best but VPNs, complex network setups, etc. may not be perfectly classified.
API Reference
NetStatusService
Method | Returns | Description |
---|---|---|
observeNetStatus() |
Stream<NetStatus> |
Deduplicated status updates |
observeNetStatusWithInitial() |
Stream<NetStatus> |
Include last status immediately |
checkNow() |
Future<NetStatus> |
Force immediate check |
getCurrentNetType() |
Future<NetType> |
Get connection type |
lastStatus |
NetStatus? |
Last known status (sync) |
lastCheckTime |
DateTime |
When last check occurred |
dispose() |
void |
Clean up resources |
Enums
NetStatus
noNetwork
: No network interfacenetworkOnly
: Network but no internetinternet
: Full internet access
NetType
unknown
: Cannot determinewifi
: WiFi connectionmobile
: Cellular dataethernet
: Wired connectionvpn
: VPN detected
Android permissions
Ensure you have:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Optionally:
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
License
MIT
Libraries
- netstatus
- netstatus public API: unified network + Internet status with configurable checks.