Mastering Real-Time Connectivity: Detecting True Internet Access in Flutter
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
The Network Illusion: Why “Connected” ≠ “Online” in Flutter
If you’ve ever opened your Flutter app at an airport, hotel, or coffee shop and seen it stubbornly refuse to load data despite showing a strong WiFi connection, you’ve encountered the network illusion. Your device is connected to a local network, but that network has no real internet access. This happens with captive portals (those “click here to connect” WiFi pages), malfunctioning routers, or ISP outages.
The root problem? Most popular connectivity plugins only check if you’re connected to a network interface (WiFi, cellular), not whether that connection can actually reach the internet. This leads to a poor user experience: your app thinks it’s online, attempts API calls, and fails with cryptic timeouts or errors.
The Naïve Approach (And Why It Fails)
Many developers start with a simple ping or a single HTTP request:
Future<bool> hasInternet() async {
try {
final response = await http.get(Uri.parse('https://www.google.com'));
return response.statusCode == 200;
} catch (e) {
return false;
}
}
This is better than checking the network interface alone, but it has significant flaws:
- Single Point of Failure: If Google is down or blocked in your region, your app incorrectly reports no internet.
- No Persistent Monitoring: This is a one-time check. Network conditions change.
- High Latency: You have to wait for the request to complete (or timeout) every time you check.
Building a Robust Real-Time Connectivity Checker
A reliable solution needs to be continuous, efficient, and intelligent. Let’s build a ConnectionManager class that embodies these principles.
First, add the necessary dependency to your pubspec.yaml:
dependencies:
connectivity_plus: ^5.0.2
http: ^1.1.0
Now, let’s create our connectivity service:
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:http/http.dart' as http;
class ConnectionManager {
final List<Uri> _testUris = [
Uri.parse('https://www.gstatic.com/generate_204'), // Google's 204 endpoint
Uri.parse('https://connectivitycheck.android.com/generate_204'),
Uri.parse('https://clients3.google.com/generate_204'),
];
final StreamController<bool> _connectionController =
StreamController<bool>.broadcast();
Timer? _pollingTimer;
bool _lastStatus = false;
Stream<bool> get connectionStream => _connectionController.stream;
bool get currentStatus => _lastStatus;
Future<bool> _performRealCheck() async {
// First, check if we have any network interface at all
final connectivityResult = await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.none) {
return false;
}
// Try multiple endpoints to avoid single-point failures
for (final uri in _testUris) {
try {
final response = await http.get(uri).timeout(
const Duration(seconds: 3),
);
// A 204 status or any successful 2xx status indicates real connectivity
if (response.statusCode == 204 ||
(response.statusCode >= 200 && response.statusCode < 300)) {
return true;
}
} catch (_) {
// Try the next endpoint
continue;
}
}
return false;
}
void startMonitoring({Duration interval = const Duration(seconds: 5)}) {
// Stop any existing timer
stopMonitoring();
// Perform initial check
_checkAndNotify();
// Set up periodic checks
_pollingTimer = Timer.periodic(interval, (_) => _checkAndNotify());
}
Future<void> _checkAndNotify() async {
final hasConnection = await _performRealCheck();
// Only notify listeners if the status changed
if (hasConnection != _lastStatus) {
_lastStatus = hasConnection;
_connectionController.add(hasConnection);
}
}
void stopMonitoring() {
_pollingTimer?.cancel();
_pollingTimer = null;
}
void dispose() {
stopMonitoring();
_connectionController.close();
}
}
Key Design Decisions Explained
-
Multiple Endpoints: We test against three reliable endpoints. If one fails (blocked, down, or unreachable), others can still confirm connectivity. The 204 status endpoints are ideal—they return no content, minimizing data usage.
-
Two-Layer Check: We first use
connectivity_plusto rule out the obvious case of no network interface. This is cheap and fast. Only if that passes do we proceed to the more expensive HTTP checks. -
Change-Only Notification: The manager only broadcasts updates when connectivity actually changes. This prevents unnecessary widget rebuilds.
-
Configurable Polling: The 5-second default interval balances responsiveness with battery/data usage. Adjust based on your app’s needs.
Integrating with Your App
Here’s how to use this in a typical Flutter app:
class MyApp extends StatelessWidget {
final ConnectionManager connectionManager = ConnectionManager();
MyApp({super.key}) {
connectionManager.startMonitoring();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: StreamBuilder<bool>(
stream: connectionManager.connectionStream,
initialData: false,
builder: (context, snapshot) {
final isOnline = snapshot.data ?? false;
return Scaffold(
appBar: AppBar(
title: const Text('Network Aware App'),
backgroundColor: isOnline ? Colors.green : Colors.orange,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isOnline ? Icons.cloud : Icons.cloud_off,
size: 80,
color: isOnline ? Colors.green : Colors.grey,
),
const SizedBox(height: 20),
Text(
isOnline
? 'You have real internet access'
: 'No internet connection\n(Captive portal or offline network)',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
if (!isOnline) ...[
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
// Guide user to open browser for captive portal
},
child: const Text('Open WiFi Login Page'),
),
],
],
),
),
);
},
),
);
}
}
Common Pitfalls to Avoid
-
Over-Polling: Checking too frequently (e.g., every second) drains battery and data. For most apps, 5-10 second intervals are sufficient.
-
Relying on a Single Service: Don’t depend on just one endpoint. Services go down, get blocked, or become slow.
-
Ignoring Network Changes: Remember to check when the app resumes from background, not just on a timer.
-
Blocking the UI: Always perform checks asynchronously. Never
awaitconnectivity checks in your build methods. -
Forgetting to Clean Up: Always cancel timers and close streams when they’re no longer needed to prevent memory leaks.
When to Use This Pattern
This approach is particularly valuable for:
- Apps that sync data in the background
- Video/audio streaming applications
- Real-time collaboration tools
- Any app where offline functionality differs significantly from online mode
For simpler apps that only need occasional checks, consider using the ConnectionManager on-demand rather than continuously. Call _performRealCheck() only when you’re about to make an API request.
By implementing a robust connectivity checker, you transform a frustrating user experience into a transparent, informative one. Your users will appreciate knowing exactly why their data isn’t loading, and you’ll reduce support requests about “the app not working on WiFi.”
Remember: true connectivity checking isn’t about adding complexity—it’s about respecting the user’s actual network conditions and adapting gracefully to them.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations
Many Flutter apps need to display content that changes based on user locale, but also comes from a backend (like Firebase). This post will explore best practices for fetching and integrating dynamic, localized content from a backend, ensuring a seamless user experience across different languages and regions without hardcoding translations.
Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues
Developers frequently encounter cryptic type mismatch errors like 'The argument type X can't be assigned to the parameter type Y' or '_InternalLinkedHashMap has no instance method 'cast''. This post will demystify these common Flutter/Dart type errors, explain their root causes (e.g., conflicting imports, dynamic typing pitfalls, JSON deserialization issues), and provide practical solutions to diagnose and fix them, improving code robustness and reducing debugging time.
Solving Flutter Web Memory Leaks: A Practical Guide to Identifying and Fixing Performance Issues
Flutter Web applications can suffer from increasing memory usage over time, leading to performance degradation. This post will delve into common causes of memory leaks in Flutter Web, provide practical debugging techniques using browser developer tools and Dart DevTools, and offer actionable strategies to identify and fix these issues for a smoother user experience.