Mastering Offline-First: Solving 'Connected to WiFi' ≠ 'Has Internet' in Flutter
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
Building a Flutter app that works reliably in all network conditions is a hallmark of great UX. We’ve all implemented a network check, only to watch our app get stuck on a loading spinner despite showing a strong WiFi signal. The core issue is a classic misdiagnosis: checking for network connectivity is not the same as checking for internet access. This post will dissect this problem and walk you through robust strategies to master offline-first functionality in Flutter.
The Problem: “Connected to WiFi” ≠ “Has Internet”
The typical approach involves using a popular package to check the device’s network state. It might tell you the device is connected to WiFi or mobile data. However, this only confirms a link to a local network adapter. It says nothing about whether that network has a pathway to the public internet.
Here are the common scenarios where this fails:
- Captive Portals: Airports, hotels, and coffee shops. Your device is connected to the WiFi router, but all traffic is intercepted until you sign in via a web page.
- Router Issues: A home WiFi router that’s lost its upstream internet connection (e.g., ISP outage).
- Restrictive Networks: Corporate or institutional networks that block specific domains or ports your app relies on.
Relying solely on a basic connectivity check leaves your app in a confused state, often attempting doomed network calls that timeout, leading to a poor user experience.
The Flawed Foundation: Why Basic Connectivity Checks Aren’t Enough
Many apps start with a simple check using packages like connectivity_plus. While excellent for determining the type of network interface (WiFi, mobile, none), it is often misused as an internet availability check.
// This is NOT a reliable internet check!
Future<bool> checkConnection() async {
final connectivityResult = await Connectivity().checkConnectivity();
return connectivityResult != ConnectivityResult.none;
}
The function above will return true when connected to a WiFi router with a captive portal, giving you a false positive. Your app thinks it’s online, but its API calls will hang.
The Robust Solution: Probing for True Internet Access
The solution is to augment the network state check with an active probe. This means making a lightweight, definitive network request to a reliable endpoint to confirm true internet reachability.
Strategy 1: The DIY Approach with http
You can implement a basic probe yourself using the http package. The key is to choose a reliable, low-latency endpoint (like a well-known public API or your own server’s health check) and set a short timeout.
import 'package:http/http.dart' as http;
Future<bool> hasRealInternetAccess() async {
// Use a reliable, fast endpoint. Google's is common, but consider your own.
const probeUrl = 'https://www.gstatic.com/generate_204'; // Returns a 204 status.
try {
final response = await http
.get(Uri.parse(probeUrl))
.timeout(const Duration(seconds: 3));
// A successful response (2xx) generally means internet is working.
return response.statusCode >= 200 && response.statusCode < 300;
} on Exception {
// Timeout, SocketException, etc. - No internet.
return false;
}
}
// Use it in combination with a connectivity check for better UX.
Future<void> loadData() async {
final connectivity = Connectivity();
final hasNetwork = await connectivity.checkConnectivity() != ConnectivityResult.none;
if (!hasNetwork) {
// Show immediate offline UI.
showOfflineBanner();
return;
}
// We have a network interface, but is there internet?
if (await hasRealInternetAccess()) {
// Safe to call your API.
final data = await fetchApiData();
updateUI(data);
} else {
// Likely a captive portal or router issue.
showCaptivePortalWarning();
}
}
Strategy 2: Using a Dedicated Package
Managing timeouts, platforms, and probe logic can get complex. Dedicated packages wrap this functionality neatly. One such package is internet_connection_checker_plus. It performs the active probe for you.
import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart';
Future<void> initApp() async {
// Create an instance.
final internetChecker = InternetConnectionCheckerPlus();
// Perform a one-off check.
bool isActuallyConnected = await internetChecker.hasConnection;
if (isActuallyConnected) {
// Proceed.
}
// Or, listen to continuous changes (great for reactive UIs).
internetChecker.onStatusChange.listen((InternetStatus status) {
switch (status) {
case InternetStatus.connected:
// Hide offline banner, sync data.
break;
case InternetStatus.disconnected:
// Show offline banner, disable actions.
break;
}
});
}
Important Consideration: A successful probe to a public server (like gstatic.com) confirms general internet access, but not necessarily access to your specific API. For the highest reliability in business logic, your final check should be a guarded attempt to call your own API’s health endpoint.
Building an Offline-First Architecture
With reliable internet detection, you can build a proper offline-first app. The pattern is straightforward:
- Check Network State: Use
connectivity_plus(or similar) to quickly show “No Network” UI. - Verify Internet Access: Use an active probe (DIY or package) to confirm real internet. Handle the “Captive Portal / No Internet” state.
- Sync Data: Only attempt to sync with your backend when both checks pass.
- Cache Reliably: Use a persistent database (like
isar,hive, orsqflite) to store data locally. Your UI should always render from this local cache. - Queue Actions: User actions that require the network should be stored in a pending queue (e.g., using
workmanageror a local DB table) and executed when connectivity is restored.
By moving beyond simple connectivity checks and implementing active internet probing, you eliminate a major source of user frustration. Your app will correctly identify captive portals, handle spotty connections gracefully, and provide a seamless, resilient experience that works as well on airport WiFi as it does on a home network.
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.