Mastering Connectivity: How to Reliably Detect and Handle Internet Status in Flutter Apps
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
Building a Flutter app that works flawlessly in the real world means preparing for the number one real-world condition: spotty internet. Users move between Wi-Fi and cellular data, enter tunnels, or have simply run out of data. An app that crashes, hangs, or shows confusing errors when offline creates a frustrating experience. Conversely, an app that gracefully informs the user, queues actions, and syncs when back online feels polished and reliable. So, how do we master connectivity in Flutter? Let’s break it down.
The Core Challenge: Beyond Local Connectivity
The most common initial mistake is checking only for a network adapter’s status (e.g., is Wi-Fi on?). This tells you if the device is capable of connecting to a network, but not if it has a working internet connection. The user could be connected to a Wi-Fi router that has no internet access itself (the classic “captive portal” scenario). A reliable check must involve a handshake with a remote server.
Strategy 1: The One-Off Check with connectivity_plus and http
For a simple, one-time check—like when the app starts or before a major download—a combination of packages works well. connectivity_plus tells you about the network state, and a quick HTTP call confirms real internet access.
First, add the dependencies to your pubspec.yaml:
dependencies:
connectivity_plus: ^5.0.0
http: ^1.2.0
Here’s a utility function you can call from anywhere:
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:http/http.dart' as http;
Future<bool> hasActualInternetConnection() async {
// Step 1: Check for any network connectivity
final connectivityResult = await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.none) {
return false; // Definitely offline
}
// Step 2: Try to reach a reliable server
try {
// Using a well-known, low-latency server (Google's public DNS)
final response = await http
.get(Uri.parse('https://www.google.com'))
.timeout(const Duration(seconds: 5));
// A successful response (status code 200-299) means we're online.
return response.statusCode >= 200 && response.statusCode < 300;
} on Exception catch (_) {
// Timeout, SocketException, etc. mean the request failed.
return false;
}
}
You’d use it like this before a critical operation:
if (await hasActualInternetConnection()) {
await fetchFreshDataFromApi();
} else {
showOfflineMessage();
}
The Caveat: This is a point-in-time check. The user’s connection could drop immediately after this check succeeds.
Strategy 2: Continuous Monitoring with a Stream
For a reactive app that needs to adapt as connectivity changes, we need a continuous stream of connectivity status. This is perfect for updating a UI banner, pausing/resuming network requests, or enabling/disabling certain buttons.
We can create a simple service class that provides this stream. We’ll again use connectivity_plus for network events and combine it with periodic reachability checks for accuracy.
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:http/http.dart' as http;
class NetworkStatusService {
// A controller for our custom stream. We'll expose the stream publicly.
final _networkStatusController = StreamController<bool>.broadcast();
Stream<bool> get networkStatusStream => _networkStatusController.stream;
// A timer for periodic checks while we appear to be connected.
Timer? _periodicCheckTimer;
final Connectivity _connectivity = Connectivity();
NetworkStatusService() {
// Listen to connectivity state changes (Wi-Fi -> Mobile -> None, etc.)
_connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
}
void _updateConnectionStatus(ConnectivityResult result) async {
bool isActuallyConnected = false;
if (result != ConnectivityResult.none) {
// We have a network type, but need to verify real internet.
isActuallyConnected = await _performReachabilityCheck();
}
// If result is 'none', isActuallyConnected remains false.
// Push the new state to all stream listeners.
_networkStatusController.sink.add(isActuallyConnected);
// Manage the periodic check timer.
_periodicCheckTimer?.cancel();
if (isActuallyConnected) {
// While online, check every 10 seconds to catch sudden drops.
_periodicCheckTimer = Timer.periodic(const Duration(seconds: 10), (_) async {
final currentStatus = await _performReachabilityCheck();
_networkStatusController.sink.add(currentStatus);
});
}
}
Future<bool> _performReachabilityCheck() async {
try {
final response = await http
.get(Uri.parse('https://connectivitycheck.gstatic.com/generate_204'))
.timeout(const Duration(seconds: 3));
// A 204 No Content is a common success response for these checks.
return response.statusCode == 204;
} on Exception {
return false;
}
}
void dispose() {
_periodicCheckTimer?.cancel();
_networkStatusController.close();
}
}
Using the Service:
Initialize this service high up in your widget tree (e.g., in a StatefulWidget’s initState or using a dependency injection provider like Provider or get_it). Then, listen to the stream:
@override
void initState() {
super.initState();
_subscription = networkStatusService.networkStatusStream.listen((isConnected) {
setState(() {
_isOnline = isConnected;
});
// You could also trigger logic here, like retrying failed requests.
});
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
Best Practices for a Seamless UX
- Visual Feedback: Use a non-obtrusive but clear banner or indicator to show offline status. Don’t rely on a full-screen dialog that blocks interaction.
- Cache Intelligently: Use packages like
dio(with its interceptors) orhttpwith a cache layer (dio_http_cache,flutter_cache_manager) to serve stale data when offline, with clear labeling. - Queue User Actions: For apps like note-takers or task managers, allow users to perform actions offline. Store these actions locally (using
hive,sqflite, orisar) and sync them when the connection is restored. - Be Smart About Retries: Don’t bombard a failing endpoint. Use exponential backoff for automatic retries on network-sensitive operations.
Common Pitfall to Avoid
Over-checking: Polling a server every second is terrible for battery life and data usage. The pattern shown above—listening to system events and doing occasional verification checks while online—provides a good balance of accuracy and efficiency.
By moving beyond simple connectivity detection to monitoring actual internet reachability and designing your app’s logic around a stream of this state, you build resilience into the core of your application. This transforms connectivity handling from an afterthought into a key feature that significantly boosts your app’s perceived quality and reliability.
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.