A phone can be connected to Wi-Fi and still have no usable internet. A captive portal, broken DNS, VPN, or server outage can make requests fail while connectivity appears healthy.
The opposite also happens: a connectivity listener reports a brief transition while an active request succeeds.
That is why I treat connectivity as a UX signal. The request result remains the source of truth.
Separate network availability from request success#
abstract interface class ConnectionService {
Stream<ConnectionStatus> watch();
Future<ConnectionStatus> current();
}
enum ConnectionStatus {
online,
offline,
}
This service can use a package that checks reachability. Repositories should still catch timeout, server, and authentication failures.
Do not skip every request because the latest connectivity value says offline. It may be stale, and local networks can change quickly.
Stabilize noisy changes#
Connectivity can switch several times while the device moves between networks:
Stream<ConnectionStatus> stableConnection(
Stream<ConnectionStatus> source,
) {
return source
.distinct()
.debounceTime(const Duration(seconds: 2));
}
A delay prevents an offline banner from flashing during a short transition. Product requirements may need asymmetric behavior: show offline quickly, but wait before hiding it.
Stream<ConnectionStatus> stabilize(
Stream<ConnectionStatus> source,
) {
return source.switchMap((status) {
final delay = status == ConnectionStatus.offline
? const Duration(milliseconds: 300)
: const Duration(seconds: 2);
return Stream.value(status).delay(delay);
});
}
Test the exact timing rather than choosing it by feel.
Preserve useful stale content#
When a refresh fails, clearing the screen often creates a worse offline experience:
final result = await repository.refresh();
result.fold(
(failure) => emit(
state.copyWith(
refreshFailure: failure,
// Existing items remain visible.
),
),
(items) => emit(
state.copyWith(
items: items,
clearRefreshFailure: true,
),
),
);
Show a small “Could not refresh” message with Retry while retaining known data. Use a full offline screen only when the feature has nothing useful to display.
Retry based on failure type#
Connectivity returning does not mean every failed operation should replay. Retrying a safe GET is different from retrying a payment or form submission.
Queue offline work only when:
- the operation has clear idempotency;
- the user understands it is pending;
- data conflicts can be resolved;
- authentication remains valid;
- retries have a limit.
For ordinary screens, a visible Retry button is often the safest behavior.
Do not create global navigation loops#
A global connection listener that replaces every route with No Network can interrupt forms, playback, and cached content. It can also repeatedly navigate as the signal changes.
Prefer a banner or feature-level state. Reserve a blocking offline screen for an application that truly cannot function without an initial connection.
Test real failure combinations#
Cover:
- Wi-Fi without internet;
- server unavailable while internet works;
- reconnect during a request;
- brief network handoff;
- cached data with refresh failure;
- app resume after a long offline period;
- repeated online and offline emissions;
- logout while a retry is pending.
The connectivity signal helps explain and anticipate failure. It does not replace handling the response from the server the user is actually trying to reach.