← Back to posts Cover image for Mastering Connectivity: How to Reliably Detect and Handle Internet Status in Flutter Apps

Mastering Connectivity: How to Reliably Detect and Handle Internet Status in Flutter Apps

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

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

  1. 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.
  2. Cache Intelligently: Use packages like dio (with its interceptors) or http with a cache layer (dio_http_cache, flutter_cache_manager) to serve stale data when offline, with clear labeling.
  3. Queue User Actions: For apps like note-takers or task managers, allow users to perform actions offline. Store these actions locally (using hive, sqflite, or isar) and sync them when the connection is restored.
  4. 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

Cover image for Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

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.

Cover image for Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

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.