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's Hidden Gem: Building Powerful Linux Desktop Apps with Ease
Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.
Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows
Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.
Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows
Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.