skip to content

Offline & Retries

connectivity_plus reports the network interface, not whether a server is reachable, so apps still need retries with backoff and a cached last response. Interviewers probe flaky-network handling.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Flutter, why can't connectivity_plus's onConnectivityChanged stream tell you whether your API is reachable, and how should an app use it instead?

level: juniorimportance: must knowfreq 58%

answer

  1. radio status, not a round trip
  2. captive-portal Wi-Fi still says wifi
  3. a list of results since 6.0.1
  4. none only ever appears alone
  5. the real request is the probe

basics

~20 s

connectivity_plus reports which network interfaces are up (wifi, mobile, none), not whether packets reach your server. Captive portals, dead DNS or a down backend all look connected, so treat it as a hint and let the real request decide.

solid answer

~40 s

`Connectivity().onConnectivityChanged` emits a `List<ConnectivityResult>` describing the active transports — `wifi`, `mobile`, `ethernet`, `vpn` and so on. It never makes a round trip, so hotel Wi-Fi behind a captive portal, a broken DNS server or your own backend being down all still report `wifi`. The plugin's own docs say not to use `checkConnectivity()` to decide whether a request will work. So I always send the request and handle timeouts and errors; I use the stream only as a hint — to show an "offline" banner when the list is just `[ConnectivityResult.none]`, and to retry immediately when an interface comes back instead of waiting out the backoff.

code

dart · 43 lines
dart
import 'dart:async';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';

class OfflineHint extends StatefulWidget {
  const OfflineHint({super.key, required this.onInterfaceBack});

  final VoidCallback onInterfaceBack;

  @override
  State<OfflineHint> createState() => _OfflineHintState();
}

class _OfflineHintState extends State<OfflineHint> {
  StreamSubscription<List<ConnectivityResult>>? _sub;
  bool _noInterface = false;

  @override
  void initState() {
    super.initState();
    _sub = Connectivity().onConnectivityChanged.listen((results) {
      final up = results.hasConnectivity;
      if (up && _noInterface) widget.onInterfaceBack(); // a hint: retry now
      setState(() => _noInterface = !up);
    });
  }

  @override
  void dispose() {
    _sub?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (!_noInterface) return const SizedBox.shrink();
    return const ListTile(
      leading: Icon(Icons.cloud_off),
      title: Text('No network interface. Sending resumes when one returns.'),
    );
  }
}

go deeper

for a junior

Recall that the plugin reports interfaces such as wifi, mobile and none, and that a captive portal still reads as wifi. Say that the request's own result decides success.

for a middle

Explain the list-shaped API, when none appears, the distinct filtering, and why the stream suits a banner and an early retry but not a gate on requests.

for a senior

Show the production view: flapping NWPathMonitor values, missed Android background changes, debouncing the banner, and resetting backoff when an interface returns.

for a principal

Frame reachability as something only the request can prove, and argue for UX that stays usable on a degraded link rather than hard offline modes keyed to a radio flag.

## What the plugin actually reports `connectivity_plus` (7.3 at the time of writing) is a federated plugin that asks each platform which **network interfaces** are currently usable. It exposes two entry points on the singleton `Connectivity()`: - `checkConnectivity()` — a one-off `Future<List<ConnectivityResult>>`. - `onConnectivityChanged` — a `Stream<List<ConnectivityResult>>` that emits whenever the set of transports changes. The plugin applies `Stream.distinct` with a list-equality check, so identical consecutive lists are dropped. The values are transports: `wifi`, `mobile`, `ethernet`, `vpn`, `bluetooth`, `satellite`, `other` and `none`. Since **6.0.1** the result is a **list**, because several transports can be active at once (for example `mobile` plus `vpn`). `none` only appears as the single element of the list when nothing is up, and 7.3.0 added a `hasConnectivity` extension on the list that encodes exactly that rule. A leftover pre-6 comparison such as `result == ConnectivityResult.wifi` now compares a list with an enum value and is always false; use `contains` instead. ## Why connected is not reachable None of this involves sending a packet to your server. The operating system knows a radio is associated and has an address; it does not know whether your API answers. The plugin's README says so directly: connection type availability does not guarantee internet access. | Situation | What the plugin reports | Does your API call work? | |---|---|---| | Hotel Wi-Fi before the captive-portal login | `[wifi]` | No | | One bar of signal at the edge of a field | `[mobile]` | Maybe, slowly, with drops | | DNS server down or corporate proxy blocking | `[wifi]` or `[ethernet]` | No | | Your backend is deploying or overloaded | anything | No, you get 5xx or a timeout | | Airplane mode | `[none]` | No | Only the last row is one where the plugin's answer is decisive — and even then, the interface can come back a second later. ## How to use it well The **request itself is the only reliable probe**. The pattern that holds up is: 1. Always send the request, with a timeout and error handling, whatever the plugin says. 2. Classify failures (connection error, timeout, 5xx, 4xx) and apply a retry policy to the retryable, idempotent ones. 3. Listen to `onConnectivityChanged` and, when the list goes from `[none]` to something with `hasConnectivity`, **wake up** pending retries now instead of waiting out a long backoff delay. 4. Use `[none]` to show a non-blocking "offline" hint, not to hard-block the UI. 5. If a screen truly needs a reachability answer, make a cheap request to **your own** backend with a short timeout and treat the answer as valid only for a moment. What you should not do is gate every call on `checkConnectivity()`: the plugin's own documentation says not to use that result to decide whether a request can succeed, and it adds a platform-channel call in front of every request. ## Platform caveats worth knowing - **Android**: since Android 8 connectivity broadcasts are not delivered to backgrounded apps, so the stream can miss changes while the app is in the background; re-check with `checkConnectivity()` when the app resumes. - **iOS simulator**: the stream may not update when Wi-Fi status changes — a known simulator-only issue. Test on a device. - **iOS and macOS**: `NWPathMonitor` can report flapping values such as `none` followed by `wifi` right after reconnecting. `distinct` removes repeats, not flaps, so debounce if the banner flickers. - **Web**: the plugin uses the browser's NetworkInformation API and falls back to `navigator.onLine`, which yields only `wifi` or `none`. - `vpn` is not reported on iOS and macOS; those platforms return `other` for it. ## In a field-survey app Picture surveyors submitting forms from farms with patchy coverage. The phone happily reports `[mobile]` while every request times out. If the submit button were disabled until the plugin said "online", it would look enabled and still fail; if it were gated on `wifi`, it would refuse to try on a usable cellular link. The robust design lets the submit run, surfaces a clear error with the form kept intact, retries safe calls with backoff, and uses the connectivity stream only to retry promptly when a signal returns. The subscription is created in `initState` and cancelled in `dispose`, like any other stream subscription held by a `State`.

  • Why does connectivity_plus return a list instead of a single ConnectivityResult?
    Since 6.0.1 the plugin reports every active transport, because a device can be on `mobile` and `vpn` at once, or `mobile` plus `satellite` on newer iOS and Android. Code must use `contains` or the `hasConnectivity` extension; `none` appears only as the sole element when nothing is up.
  • If a screen really needs to know the backend is reachable, what would you do?
    Make a cheap request to your own backend — a health or HEAD endpoint — with a short timeout, and treat success as "reachable right now" only. Probing a third-party host proves nothing about your API, and the answer goes stale immediately, so the real request still needs its own error handling.
  • Why check connectivity again when an Android app returns to the foreground?
    The plugin's README notes that since Android 8 connectivity broadcasts are not delivered to backgrounded apps, so the stream may have missed a change. Calling `checkConnectivity()` on resume resynchronises the hint before the user acts on it.

The plugin is like checking that your phone shows signal bars: it tells you the radio is associated with a tower, not that the person you are calling will pick up. You only know that by dialling.

saying these in an interview costs you the question

  • If connectivity_plus reports wifi, the API call is going to succeed.
  • Gate every request on checkConnectivity() instead of handling network errors.
  • ConnectivityResult.none can appear in the list alongside wifi.
  • Comparing the result with == ConnectivityResult.wifi still works in current versions.
  • The stream keeps delivering changes while an Android app is backgrounded.
open as a page

In a flaky-network Flutter app, how should a screen keep showing the last good response after a refresh fails, and which states does it need?

level: middleimportance: should knowfreq 45%

basics

~20 s

Keep the last successfully parsed response and its fetch time, and on a failed refresh show that data marked stale with a non-blocking error and retry. Only a failure with nothing cached gets a full-screen error.

open as a page

With package:http's RetryClient, which failures are retried by default, how long does it wait between attempts, and what must you configure yourself?

level: middleimportance: should knowfreq 30%

basics

~20 s

RetryClient retries 3 times by default, only when the response status is 503; thrown errors such as a dropped connection are not retried. Delays are 500 ms growing 1.5x with no jitter, and it retries any HTTP method.

open as a page

In a Flutter app using dio, how would you write a retry interceptor that retries only safe failures with capped, jittered exponential backoff?

level: seniorimportance: should knowfreq 42%

basics

~10 s

Override Interceptor.onError: skip non-idempotent methods, cancels and 4xx; retry connection errors, timeouts and 502-504 by waiting a random delay under a capped exponential bound, then resolving with dio.fetch(requestOptions), counting attempts in requestOptions.extra.

open as a page