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
Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB
Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.
Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls
Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.
Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter
Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.