How do you integrate Flutter web with REST/GraphQL + offline?

Design a Flutter web integration for REST/GraphQL with caching, offline support, and resilient error recovery.
Build Flutter web integration that blends REST/GraphQL, a layered caching strategy, offline support, and robust error recovery.

answer

Successful Flutter web integration separates transport concerns from UI. Use http/dio for REST or GraphQL clients (e.g., graphql_flutter) behind a repository layer. Add a multi-tier caching strategy: memory (per view), persisted (IndexedDB via hive/isar), and conditional requests (ETag) for freshness. Provide offline support with queued mutations and replay on reconnect. Guard error recovery using retry w/ backoff, circuit breakers, and user-visible fallbacks so the app degrades gracefully.

Long Answer

A production-grade Flutter web integration treats the server as source of truth while giving users fast, resilient experiences. I build a layered stack that abstracts transport (REST and GraphQL), centralizes policy (caching, retries, auth), and keeps widgets declarative. The pillars are: repositories, normalized caching, offline support, and disciplined error recovery that prevents cascades.

1) Architecture: clean boundaries
Adopt a clean architecture:

  • Data sources: REST (dio with interceptors) and GraphQL (graphql_flutter with links) live behind a common interface.
  • Repositories: consolidate fetching, write policies, and entity mapping.
  • State management: Riverpod/Bloc/Cubit for UI state; repositories expose streams/futures. This separation lets views stay small and testable while the integration layer enforces a consistent caching strategy and backoff rules.

2) Caching strategy: memory → persisted → network
I implement a three-tier cache:

  • Memory cache (per page/scope) for instant renders and Suspense-like UX via FutureProvider/StreamProvider.
  • Persisted cache using IndexedDB via hive or isar (web-safe). This enables warm starts and offline support.
  • Network with conditional requests: ETag/If-None-Match or GraphQL @defer/@stream where supported.
    Cache keys include route params and filters, so invalidations are surgical. Data is normalized by id (e.g., collection:itemId) to avoid duplication; repositories hydrate models from persisted cache first, then refresh in background (stale-while-revalidate).

3) REST and GraphQL composition
For REST, I use dio interceptors for auth, logging, ETag, and retry policies. For GraphQL, I configure HttpLink + AuthLink + RetryLink + ErrorLink and choose NormalizedInMemoryCache backed by a Hive store. Mixed backends are wrapped by repositories so widgets never care whether the call was REST or GraphQL. When backends support subscriptions, I add WebSocketLink for live updates and fold events into the same normalized cache.

4) Writes: optimistic/pessimistic and idempotency
Low-risk mutations (rename, preference toggles) are optimistic: update memory and persisted cache immediately, then commit server-side; rollback on failure with a toast. Destructive operations use pessimistic flow (wait for server). Every mutation carries a deterministic clientId (UUID) so the API can deduplicate—this is crucial for offline support and retry storms. Repositories expose a queue for pending writes; on reconnect, they replay in order, reconcile with server versions, and emit canonical entities.

5) Preventing race conditions and staleness
Races happen when a slower response overwrites a newer one. I tag requests with request epochs and cancel stale inflight calls in repositories. For GraphQL, I let RetryLink/FetchPolicy.cacheAndNetwork drive revalidation; for REST, I use ETag and If-Match on PUT/PATCH to enforce versions. Staleness is controlled with per-query TTLs and background refresh; lists use cursor pagination to avoid “jumping.” On route change, scoped providers dispose caches, and inflight requests are aborted.

6) Error recovery without waterfalls
I implement resilient error recovery:

  • Retry with exponential backoff (jitter) for idempotent reads; no retry for known user errors (4xx).
  • Circuit breaker in repositories after consecutive failures; switch to cached snapshot and surface a banner.
  • Granular boundaries: page-level loaders, component-level fallbacks; one failed panel doesn’t blank the whole screen.
  • User guidance: actionable messages (“Tap to retry”, “Working offline”). All errors are structured (code, message, retryable, offline) to drive consistent UI decisions.

7) Offline support end-to-end
On web, Service Worker + IndexedDB enable true offline support. I pre-cache shell assets (Flutter’s PWA mode) and critical queries (home, profile). Repositories expose “availability” streams; when offline, reads serve from persisted cache and mark snapshots stale. Mutations land in the queue with a local echo; on reconnect, I replay them, handle conflicts using version fields/ETags, and, if needed, prompt a merge UI (field-level diff) before finalizing.

8) Security and compliance
Never persist secrets. Tokens live in session storage or memory with refresh flows; persisted caches store only non-sensitive fields, optionally encrypted at rest. For GraphQL, I prefer persisted queries to cut payload and reduce PII exposure. Error logs strip payloads to avoid leaking data.

9) Observability and testing
Network link logs (redacted), repository metrics (hit rate, queue depth, retry counts), and analytics events show health. Tests cover: cold start from cache, SWR refresh, optimistic rollback, queued mutation replay, 412/409 conflict handling, and subscription fan-in. This suite keeps the integration honest as scope grows.

The result: a Flutter web integration that cleanly composes REST and GraphQL, uses a layered caching strategy, offers robust offline support, and turns failures into recoverable, user-friendly states.

Table

Layer Strategy Tools/Patterns Outcome
Transport REST + GraphQL behind repos dio interceptors; graphql_flutter links Unified API boundary
Caching Memory → persisted → network SWR, Hive/Isar (IndexedDB), ETag/If-None-Match Fast + fresh reads
Offline Queue + replay UUID clientId, Service Worker, retry Reliable offline support
Writes Optimistic/pessimistic Local echo + rollback; guarded deletes Snappy UX, safe data
Staleness TTL + revalidate Per-query TTLs, focus/reconnect refresh Stable, current cache
Races Cancel/epoch/version Abort inflight, ETag/If-Match Newest wins, no overwrite
Errors Backoff + breaker Exponential retry + circuit breaker Calm error recovery
Realtime Subscriptions merge WebSocketLink → normalized cache Live updates without flicker

Common Mistakes

Mirroring server data directly in widgets instead of repositories, creating tight coupling. Skipping normalization so the same record appears twice and drifts. Treating the web as always-online—no queue, no replay—so users lose work. Over-relying on global retries, causing thundering herds and rate limits. Omitting ETag/If-Match, which invites silent last-write-wins conflicts. Using one giant error screen that nukes the page (error waterfall) instead of granular fallbacks. Persisting tokens or sensitive fields in IndexedDB. Ignoring Service Worker updates, leaving users stuck on stale app shells. Lacking tests for rollback/replay, so edge cases explode in production.

Sample Answers (Junior / Mid / Senior)

Junior:
“I call REST with dio and use graphql_flutter when the backend is GraphQL. I cache with Hive so pages load offline, and I retry simple errors. Small edits are optimistic with rollback.”

Mid:
“My repositories hide REST/GraphQL and enforce a caching strategy: memory first, then Hive (IndexedDB), then network with ETag. I prevent races by canceling inflight calls and using If-Match. Mutations carry a clientId for idempotency and queue offline. Errors use backoff and circuit breakers so one failing panel doesn’t crash the page.”

Senior:
“I design for convergence: versioned entities, idempotent mutations, SWR, and queued writes with replay. Subscriptions feed a normalized cache to avoid flicker. Error recovery is structured (retryable vs not), and security keeps secrets out of storage. Metrics track hit rate, queue depth, and retries; tests cover rollback, conflicts, and PWA updates. That’s a durable Flutter web integration.”

Evaluation Criteria

Look for a layered Flutter web integration: repositories abstract REST/GraphQL; a three-tier caching strategy (memory → persisted → network) with SWR and conditional requests; explicit offline support via queues, replay, and Service Worker; and rigorous error recovery (backoff, circuit breaker, granular fallbacks). Candidates should mention versioning/ETag to stop silent conflicts and cancelation/epochs to prevent race overwrites. Realtime should route through a normalized cache. Security must exclude secrets from IndexedDB and sanitize logs. Strong answers include observability (hit rates, retry counts) and tests for rollback, replay, and conflict paths. Weak answers say “just use Hive and retry” without policies or boundaries.

Preparation Tips

Build a mini app: list/detail with REST and a second screen with GraphQL. Implement repositories, memory + Hive cache, and SWR refresh. Add ETag/If-Match and prove conflict handling with a simulated concurrent edit. Implement an optimistic rename and a pessimistic delete. Add a queued mutation store with clientId and a reconnect replay. Enable PWA Service Worker and precache shell; test true offline support. Layer granular fallbacks so one failed panel doesn’t crash the page; add backoff + circuit breaker. Instrument hit rate, retries, queue depth. Write tests covering cold start from cache, rollback, conflict, and replay. Practice a 60–90s story connecting these moves to real reliability wins.

Real-world Context

A marketplace app cut p95 time-to-content by 35% after adopting SWR with memory→Hive caching; pages rendered instantly from cache, then refreshed. An analytics portal stopped duplicate submissions during outages by adding queued mutations with clientId and server-side idempotency—offline support became a feature, not a bug. A fintech killed “ghost overwrites” using ETag/If-Match; users now see a merge prompt on conflict. A media dashboard removed “all-red” failures by adding circuit breakers and granular fallbacks—charts kept rendering while one datasource flaked. Subscriptions routed into a normalized cache removed flicker. Together, these patterns turned a fragile web build into a resilient Flutter web integration.

Key Takeaways

  • Repositories abstract REST/GraphQL; widgets stay clean.
  • Three-tier caching strategy with SWR and conditional requests.
  • Offline support = queued mutations, replay, PWA shell.
  • Version/ETag + cancelation prevent races and conflicts.
  • Structured error recovery avoids waterfalls and user churn.

Practice Exercise

Scenario:
You’re shipping a Flutter web dashboard that must work on spotty Wi-Fi, integrate both REST (reports) and GraphQL (profiles), and never lose user edits.

Tasks:

  1. Repositories: Create ReportsRepo (REST via dio) and ProfilesRepo (GraphQL via graphql_flutter). Expose getList, getById, update, subscribe.
  2. Caching strategy: Add memory cache per view and persisted Hive boxes (IndexedDB). Implement SWR: render cached snapshot immediately, then refresh in background.
  3. Conditional requests: For REST, store ETag and send If-None-Match. For updates, include If-Match to enforce versions.
  4. Offline support: Implement a mutation queue with UUID clientId. When offline, echo UI changes and enqueue; on reconnect, replay with backoff and dedupe by clientId.
  5. Realtime: Add WebSocketLink for profile changes; batch events every 100 ms and upsert into normalized cache to avoid flicker.
  6. Error recovery: Add retry with jitter for idempotent reads, a circuit breaker that flips to cached snapshot after repeated failures, and granular fallbacks so failing panels show “Tap to retry” without blanking the page.
  7. Security/observability: Keep tokens out of IndexedDB, redact payloads in logs, and record metrics (cache hit rate, queue depth, retries).


Deliverable:
A short demo + README proving fast cached loads, conflict-safe edits, resilient error recovery, and seamless offline support in a mixed REST/GraphQL Flutter web integration.

Still got questions?

Privacy Preferences

Essential cookies
Required
Marketing cookies
Personalization cookies
Analytics cookies
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.