Examples

Case Study: Network Awareness

Queries pause automatically when offline, mutations queue for later, and everything replays on reconnect: with thundering-herd prevention built in.

Objective

Context: When a device loses connectivity, a naive implementation either crashes (uncaught network exception), freezes (spinner that never resolves), or silently drops the user's last action (mutation lost forever). None of these are acceptable in a production app.

This example demonstrates Qora's network-aware layer: queries pause automatically when offline and replay on reconnect, mutations are queued and replayed in FIFO order, and reconnect replay uses controlled concurrency so that a sudden flood of paused queries doesn't spike the server.


Key Features Demonstrated

  • NetworkMode.online (default): queries pause at FetchStatus.paused when offline; replay automatically on reconnect
  • NetworkMode.offlineFirst: serves cached data immediately, schedules background revalidation when back online
  • NetworkMode.always: bypasses connectivity checks entirely (for local-DB-backed queries)
  • FetchStatus.paused: surfaces the "waiting for connection" state to the UI without any custom boolean
  • OfflineMutationQueue: FIFO queue; mutations fired offline are replayed in order on reconnect
  • ReconnectStrategy: maxConcurrent + jitter prevent thundering-herd when dozens of queries resume simultaneously
  • NetworkStatusIndicator: automatic offline banner widget, zero configuration
  • FlutterConnectivityManager: wires connectivity_plus into QoraScope in one line

Architecture Highlights

Network awareness is orthogonal to query state. A query can be in Success state (data visible) while FetchStatus is paused (connectivity lost, background revalidation waiting). The widget tree is never blocked: it always has something to render.

main.dart
└── QoraScope(
      client:              QoraClient(config: reconnectStrategy + networkMode)
      connectivityManager: FlutterConnectivityManager()    ← plugs in connectivity_plus
      lifecycleManager:    FlutterLifecycleManager(...)    ← refetch on resume
      child: NetworkStatusIndicator(                        ← offline banner, zero config
        child: MyApp(),
      ),
    )

The QoraClient is configured with a ReconnectStrategy that controls how paused queries are replayed when connectivity returns:

lib/main.dart
final client = QoraClient(
  config: QoraClientConfig(
    defaultOptions: const QoraOptions(
      networkMode: NetworkMode.online, // pause when offline, replay on reconnect
      staleTime: Duration(minutes: 5),
    ),
    reconnectStrategy: const ReconnectStrategy(
      maxConcurrent: 5, // replay 5 queries at a time
      jitter: Duration(milliseconds: 200), // random delay between batches
    ),
    debugMode: kDebugMode,
  ),
);

runApp(
  QoraScope(
    client: client,
    connectivityManager: FlutterConnectivityManager(),
    lifecycleManager:    FlutterLifecycleManager(qoraClient: client),
    child: NetworkStatusIndicator(child: const MyApp()),
  ),
);

Implementation Details

ReconnectStrategy: thundering-herd prevention

Consider an app with 20 queries active across 8 screens. The user takes the subway for 20 minutes: all 20 queries pause. On reconnect, without a strategy, 20 simultaneous requests fire. If 200 users come out of the same subway station at the same time, the server receives 4,000 simultaneous requests.

ReconnectStrategy(maxConcurrent: 5, jitter: Duration(milliseconds: 200)) staggers replay into batches of 5, with a random delay (up to 200 ms) between each batch:

lib/main.dart
const ReconnectStrategy(
  maxConcurrent: 5,
  jitter: Duration(milliseconds: 200),
)

// Named constructors for common configurations:
ReconnectStrategy.instant() // all at once: useful for tests
ReconnectStrategy.conservative() // maxConcurrent: 2, jitter: 500ms: battery-conscious

The jitter is random per client instance: so even if 1,000 devices reconnect simultaneously, their replay windows are spread across a 200 ms window, distributing the load.

FetchStatus.paused: accurate UI state without custom flags

QoraBuilder exposes FetchStatus as its third builder argument. paused means "a fetch is queued but waiting for connectivity": distinct from fetching (actively running) and idle (nothing pending). This lets widgets render an accurate offline indicator without a single bool _isOffline:

lib/features/feed/ui/feed_screen.dart
QoraBuilder<List<Post>>(
  queryKey: const ['posts'],
  fetcher: api.getPosts,
  builder: (context, state, fetchStatus) {
    return switch (state) {
      // First load, offline: nothing to show yet
      Initial() || Loading(previousData: null) when fetchStatus == FetchStatus.paused =>
          const Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.wifi_off, size: 48, color: Colors.grey),
                SizedBox(height: 12),
                Text('No connection: connect to load posts'),
              ],
            ),
          ),

      // First load, online: standard spinner
      Initial() || Loading(previousData: null) =>
          const Center(child: CircularProgressIndicator()),

      // Data available: show feed regardless of connectivity
      _ => Column(
          children: [
            // Paused: offline but data visible
            if (fetchStatus == FetchStatus.paused)
              const _OfflineBanner(),
            // Fetching: SWR background revalidation running
            if (fetchStatus == FetchStatus.fetching)
              const LinearProgressIndicator(minHeight: 2),
            if (state is Failure<List<Post>>)
              _ErrorBanner(error: state.error),
            Expanded(child: PostList(posts: state.dataOrNull!)),
          ],
        ),
    };
  },
)

Offline mutation queue: write now, sync later

Mutations fired while offline are enqueued and replayed in FIFO order on reconnect. The isOptimistic flag on MutationSuccess drives a "pending sync" visual so the UI is never misleading:

lib/features/feed/ui/compose_screen.dart
QoraMutationBuilder<Post, String, void>(
  mutator: (content) => api.createPost(content),
  options: MutationOptions(
    offlineQueue: true,
    optimisticResponse: (content) => Post.optimistic(content: content),
    onSuccess: (_, __, ___) async =>
        context.qora.invalidate(const ['posts']),
  ),
  builder: (context, state, mutate) {
    final isQueued = state is MutationSuccess && state.isOptimistic;

    return Column(
      children: [
        if (isQueued)
          const _SyncPendingBanner(), // "Will sync when online"
        SubmitButton(
          label: isQueued ? 'Queued' : 'Post',
          icon: isQueued ? Icons.schedule : Icons.send,
          onPressed: state.isPending ? null : () => mutate(_controller.text),
        ),
      ],
    );
  },
)

On reconnect, QoraClient replays the queue in FIFO order automatically. The onSuccess callback (which calls invalidate) fires only for real server confirmations (isOptimistic: false): never for the optimistic write.

NetworkMode.always for local-database queries

Not all queries need connectivity. A query backed by a local Hive box or SQLite database should never be paused:

settings_screen.dart
QoraBuilder<Settings>(
  queryKey: const ['settings'],
  fetcher: localDb.getSettings, // reads from Hive: no network needed
  options: const QoraOptions(
    networkMode: NetworkMode.always, // never pauses, never waits for connectivity
    staleTime: Duration(hours: 1),
  ),
  builder: (context, state, _) { /* ... */ },
)

This snippet is not part of the example app, which only talks to a remote API.

Mix NetworkMode.always (local) and NetworkMode.online (remote) queries freely in the same screen: Qora handles them independently.


Source Code

examples/ex_05_network_aware/
├── lib/
│   ├── main.dart                     ← QoraClient + ReconnectStrategy + QoraScope
│   ├── features/
│   │   └── feed/
│   │       ├── data/feed_api.dart    ← HTTP layer
│   │       ├── models/post.dart      ← Post model
│   │       └── ui/
│   │           ├── feed_screen.dart  ← FetchStatus.paused handling
│   │           └── compose_screen.dart ← OfflineMutationQueue + isOptimistic
│   └── shared/
│       ├── connectivity/simulated_connectivity_manager.dart ← online/offline toggle for the demo
│       └── widgets/
│           └── offline_banner.dart   ← custom banner wrapped around NetworkStatusIndicator
└── pubspec.yaml

View full source code


Copyright © 2026