← Back to posts Cover image for Mastering Offline-First: Solving 'Connected to WiFi' ≠ 'Has Internet' in Flutter

Mastering Offline-First: Solving 'Connected to WiFi' ≠ 'Has Internet' in Flutter

· 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 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:

  1. Check Network State: Use connectivity_plus (or similar) to quickly show “No Network” UI.
  2. Verify Internet Access: Use an active probe (DIY or package) to confirm real internet. Handle the “Captive Portal / No Internet” state.
  3. Sync Data: Only attempt to sync with your backend when both checks pass.
  4. Cache Reliably: Use a persistent database (like isar, hive, or sqflite) to store data locally. Your UI should always render from this local cache.
  5. Queue Actions: User actions that require the network should be stored in a pending queue (e.g., using workmanager or 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

Cover image for Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

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.

Cover image for Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

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.

Cover image for Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

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.