skip to content

Backend API Calls

How a Flutter app talks to servers: REST clients with dio or http, JSON-mapped models, WebSocket and GraphQL clients, and retry handling. Interviewers trace a feature's data flow through it.

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

explore

questions

26

In a Flutter app using graphql_flutter, how do you build a GraphQLClient and make it available to Query and Mutation widgets?

level: juniorimportance: must knowfreq 55%

answer

  1. links chained, cache attached
  2. AuthLink(getToken:).concat(HttpLink)
  3. GraphQLCache(store: HiveStore())
  4. await initHiveForFlutter() in main
  5. ValueNotifier into GraphQLProvider

basics

~10 s

Chain an AuthLink onto an HttpLink, give GraphQLClient that link and a GraphQLCache (HiveStore for persistence, after await initHiveForFlutter()), wrap it in a ValueNotifier and pass it to a GraphQLProvider above MaterialApp.

solid answer

~40 s

A `GraphQLClient` needs two things: a `link` that sends operations and a `cache`. For the link I build `HttpLink('https://api.example.com/graphql')` and put `AuthLink(getToken: () async => 'Bearer $token')` in front with `authLink.concat(httpLink)`; `getToken` runs for every request and must return the full header value. For the cache, `GraphQLCache()` defaults to an in-memory store; to persist it I call `await initHiveForFlutter()` in `main` and pass `GraphQLCache(store: HiveStore())`. The client goes into a `ValueNotifier<GraphQLClient>` handed to `GraphQLProvider`, which I place above `MaterialApp` so every `Query`, `Mutation` and `Subscription` widget, and the hooks, find it through the context. Replacing `notifier.value`, for example after logout, rebuilds the widgets that depend on it.

code

dart · 23 lines
dart
import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';

Future<void> main() async {
  await initHiveForFlutter();

  final httpLink = HttpLink('https://api.bookclub.example/graphql');
  final authLink = AuthLink(getToken: () async => 'Bearer ${await tokenStore.read()}');

  final client = ValueNotifier(
    GraphQLClient(
      link: authLink.concat(httpLink),
      cache: GraphQLCache(store: HiveStore()),
    ),
  );

  runApp(
    GraphQLProvider(
      client: client,
      child: const MaterialApp(home: ReadingListScreen()),
    ),
  );
}

go deeper

for a junior

Know the four objects: HttpLink, AuthLink, GraphQLCache and GraphQLClient, and that GraphQLProvider above MaterialApp makes the client available.

for a middle

Explain link composition, why getToken runs per request, the in-memory default store versus HiveStore, and why the provider takes a ValueNotifier.

for a senior

Plan how the client is replaced on logout, how the persisted cache is cleared, and where subscriptions split off to a WebSocketLink.

for a principal

Decide whether a GraphQL client library, plain HTTP with typed models, or code generation best fits the team and the backend contract.

## The pieces `graphql_flutter` is the Flutter layer over the standalone `graphql` package. Setting it up means building three objects and one widget: | Piece | Class | Job | |---|---|---| | transport | `HttpLink` | sends queries and mutations as HTTP requests | | request middleware | `AuthLink` | adds an auth header to each request | | cache | `GraphQLCache` with a `Store` | normalises and keeps results | | client | `GraphQLClient(link:, cache:)` | runs operations, applies policies | | provider | `GraphQLProvider(client: ValueNotifier<GraphQLClient>)` | exposes the client to the widget tree | ## Links A **link** is one step in the pipeline an operation passes through. Links are composed, and the last one must actually send the request (a **terminating link**): - **`HttpLink(uri)`** is the usual terminating link for queries and mutations. - **`AuthLink(getToken: ...)`** is a middleware link. `getToken` is called **for every request**, can be `async`, and must return the **complete header value** including any `Bearer ` prefix; the header name defaults to `Authorization` and can be changed with `headerKey`. - **`authLink.concat(httpLink)`** chains them: auth first, then HTTP. - For subscriptions, a `WebSocketLink` is added with **`Link.split((request) => request.isSubscription, wsLink, httpChain)`**; without the split, subscription operations go to the HTTP link. Because `getToken` runs per request, it can read the current token from secure storage or an auth service; the client does not need rebuilding when a token is refreshed. ## The cache and persistence `GraphQLCache` stores results **normalised** by type and id, so two queries that return the same book share one record. Its `store` decides where that data lives: 1. **Default: `InMemoryStore`**, lost when the app process ends. 2. **`HiveStore`**, backed by Hive boxes on disk, so the last results are available on the next cold start. `HiveStore` needs Hive to be initialised first. In a Flutter app you call **`await initHiveForFlutter()`** in `main` before `runApp`: it ensures the widgets binding exists, points Hive at the app documents directory (on non-web platforms), and opens the default box. Forgetting it is the classic setup failure: the store cannot open its box. ## The provider `GraphQLProvider` takes a **`ValueNotifier<GraphQLClient>`**, not a bare client. It listens to the notifier and exposes the client through an inherited widget, so: - `Query`, `Mutation` and `Subscription` widgets, and the `useQuery`/`useMutation`/`useSubscription` hooks, find the client from their `BuildContext`; - assigning a new client to `notifier.value` (after logout, or when switching environments) rebuilds the dependents with the new client; - `GraphQLProvider.of(context)` returns the notifier for code that needs the client directly, such as a repository calling `client.query`. The package recommends placing the provider **above `MaterialApp`**, so every route pushed by the navigator is inside it. ## Logging out and clearing the cache A persisted cache outlives the session that filled it, so logout needs care, especially on a shared device: - **clear the data**: `client.resetStore(refetchQueries: false)` resets the store (for `HiveStore` that clears its box); the method is marked experimental in the package; - **drop the client**: assign a fresh `GraphQLClient` to the `ValueNotifier` so no widget keeps watching queries that belonged to the previous member; - **stop the token**: make `getToken` return `null` once signed out; `AuthLink` then adds no header rather than an empty one. Skipping these steps is how one member's reading list flashes up for the next person who signs in. ## A book-club app, concretely For a book-club app the client is built once in `main`: the HTTP endpoint for queries and mutations on reviews, an `AuthLink` reading the member's token, a `HiveStore` so the reading list shows instantly on launch, and a `GraphQLProvider` wrapping the whole app. Screens then declare what they need with `Query` widgets or hooks, and never construct a client themselves.

  • Why does GraphQLProvider take a ValueNotifier rather than a GraphQLClient?
    So the client can be swapped at runtime. The provider listens to the notifier and rebuilds its inherited widget when `value` changes, so after logout you can assign a fresh client with an empty cache and every `Query` and `Mutation` below picks it up.
  • What does GraphQLCache use when you pass no store?
    An `InMemoryStore`. Results are normalised and shared across queries while the app runs but are gone after a restart. Pass `HiveStore()`, after `initHiveForFlutter()`, to keep them on disk.

saying these in an interview costs you the question

  • GraphQLCache persists to disk by default
  • AuthLink adds the Bearer prefix for you
  • getToken is called once when the client is created
  • GraphQLProvider can be placed anywhere, even below the screens that query
  • HiveStore works without initialising Hive first
open as a page

In Flutter with package:http, how do you send a GET and a JSON POST, and why reuse one http.Client instead of calling http.get each time?

level: juniorimportance: must knowfreq 62%

basics

~10 s

Call client.get(uri) or client.post(uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode(data)) on one long-lived http.Client and check response.statusCode. The top-level http.get creates and closes a new Client for every call, losing persistent connections.

open as a page

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%

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.

open as a page

In a Flutter app, how do you turn a decoded JSON API response into a typed Dart model, and why not pass Map<String, dynamic> around?

level: juniorimportance: must knowfreq 74%

basics

~20 s

Give the model class a factory fromJson(Map<String, dynamic>) that casts each key into a typed final field, and a toJson() that builds the map back. Raw maps push every typo and type mismatch to runtime, far from the API call.

open as a page

In Flutter with web_socket_channel, how do you open a WebSocket, receive and send messages, and close it when the screen goes away?

level: juniorimportance: must knowfreq 64%

basics

~10 s

Call WebSocketChannel.connect(uri), await channel.ready, listen to channel.stream for incoming messages and call channel.sink.add to send. In the State's dispose, cancel the subscription and call channel.sink.close().

open as a page

With graphql_flutter, which FetchPolicy does a Query widget use by default, and when would you choose each of the other policies?

level: middleimportance: must knowfreq 48%

basics

~20 s

A Query widget watches its query, so it uses the watchQuery default, FetchPolicy.cacheAndNetwork; a one-shot client.query defaults to cacheFirst, and mutations and subscriptions to networkOnly. Override per operation in the options or per client with DefaultPolicies.

open as a page

With dio in a Flutter gym-membership app, how do you attach the access token with an Interceptor, and what must each handler callback do?

level: middleimportance: must knowfreq 55%

basics

~10 s

Add an Interceptor whose onRequest sets options.headers['Authorization'] and then calls handler.next(options). Every callback must call exactly one of next, resolve or reject on its handler, or the request never completes.

open as a page

With json_serializable, how do you map a nested order-history payload with snake_case keys and optional fields onto Dart models?

level: middleimportance: must knowfreq 62%

basics

~10 s

Annotate each class with @JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true), give absent keys nullable types or @JsonKey(defaultValue:), use @JsonKey(name:) for odd keys, and set includeIfNull: false to omit nulls from toJson.

open as a page

In a Flutter live-scores app using web_socket_channel, how do you reconnect after drops and app backgrounding without hammering the server or losing updates?

level: seniorimportance: must knowfreq 52%

basics

~20 s

A WebSocketChannel cannot be reopened, so a repository creates a new one per attempt, detects drops via onDone or errors, retries with capped, jittered backoff, closes on backgrounding and reconnects on return, then resubscribes and fetches a snapshot.

open as a page

With graphql_flutter, how do you get typed Dart objects instead of Map<String, dynamic> from query results, and where does code generation fit?

level: middleimportance: should knowfreq 30%

basics

~10 s

Pass a parserFn to QueryOptions<TParsed> and read result.parsedData, which parses once and caches. graphql_codegen, the generator the package points to, emits typed options, variables and hooks from .graphql files, removing hand-written parsing.

open as a page

With dio in Flutter, how does a CancelToken cancel in-flight requests, and what goes wrong if you reuse a token after cancelling it?

level: middleimportance: should knowfreq 36%

basics

~20 s

Pass a CancelToken to requests and call cancel() to abort them; each throws a DioException of type cancel. A cancelled token stays cancelled, so any later request using it fails immediately; create a new token per screen or search.

open as a page

With dio in a Flutter app, what do BaseOptions baseUrl, the three timeouts and validateStatus control, and how do failures surface as DioException?

level: middleimportance: should knowfreq 48%

basics

~20 s

BaseOptions set defaults for every request of one Dio: baseUrl, connectTimeout, sendTimeout, receiveTimeout (null means no limit) and validateStatus, which by default accepts only 2xx. Failures throw DioException with a type such as connectionTimeout, badResponse, cancel or connectionError.

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

With json_serializable, how do you decode a generic response envelope such as Page<T>, and what does genericArgumentFactories generate?

level: middleimportance: should knowfreq 34%

basics

~10 s

Set @JsonSerializable(genericArgumentFactories: true) on Page<T>; the generated fromJson then takes a T Function(Object? json) fromJsonT and toJson takes an Object? Function(T value) toJsonT, which the caller supplies, for example Order.fromJson.

open as a page

With json_serializable, when do you reach for a JsonConverter class rather than @JsonKey(fromJson:, toJson:) to change a field's wire format?

level: middleimportance: should knowfreq 38%

basics

~10 s

@JsonKey(fromJson:, toJson:) suits a one-off field and takes static or top-level functions. A JsonConverter<T, S> is reusable: annotate a field, a whole class or JsonSerializable(converters:), and it also converts T inside collections.

open as a page

With web_socket_channel, how do you close a socket with a status code and reason, and how do you read why the server closed it?

level: middleimportance: should knowfreq 30%

basics

~20 s

Call channel.sink.close(code, reason), where the code must be 1000 or 3000-4999 and the reason at most 123 UTF-8 bytes. After the stream's onDone, channel.closeCode and channel.closeReason hold what the server sent; both are null before that.

open as a page

With web_socket_channel on mobile, what does IOWebSocketChannel.connect's pingInterval do, and why is it not available through WebSocketChannel.connect?

level: middleimportance: should knowfreq 28%

basics

~20 s

pingInterval makes the dart:io socket ping at that interval and close the connection when no pong arrives in time; it defaults to null, no pings. It is io-only because the portable connect must also run on the web.

open as a page

With web_socket_channel, why await channel.ready after WebSocketChannel.connect, and how does a failed connection show up in a Flutter app?

level: middleimportance: should knowfreq 42%

basics

~20 s

connect returns before the handshake; channel.ready completes when the socket is open or with an error if it failed. A failure also reaches channel.stream as a WebSocketChannelException followed by done, so both must be handled.

open as a page

In a graphql_flutter book-club app, why does a newly posted review not appear in the list after the Mutation succeeds, and how do you fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The cache merges changed fields of entities it already holds, but it cannot know a new review belongs in a cached list. Add it in MutationOptions.update with cache.readQuery and cache.writeQuery, or refetch the list query.

open as a page

In a Flutter app using dio, five parallel requests all get 401 when the access token expires; how do you refresh the token exactly once and replay them?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Use a QueuedInterceptor so onError handles one 401 at a time. The first refreshes through a plain Dio without that interceptor; later ones see the token already changed. Each replays on the plain Dio and resolves its handler.

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

In a Flutter app, the order-history screen skips frames while a multi-megabyte JSON response is decoded into models; how do you move that work off the UI isolate?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Put jsonDecode and the fromJson mapping into one top-level function that takes the response body String, and run it with compute() or Isolate.run. Decoding alone off-isolate is not enough if fromJson still runs on the UI isolate.

open as a page

With freezed, how do you deserialize a polymorphic JSON payload whose type field selects the variant, and what happens when the server sends an unknown type?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Declare a sealed freezed class with one factory constructor per variant, set @Freezed(unionKey: 'type'), map values with unionValueCase or @FreezedUnionValue, and name a catch-all with fallbackUnion, because an unknown value otherwise throws CheckedFromJsonException.

open as a page

Why would a Flutter app swap package:http's default client for cupertino_http or cronet_http, and how do you wire that in without changing call sites?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

The default IOClient uses dart:io sockets; cupertino_http (URLSession) and cronet_http (Cronet) use the platform stack, gaining VPN and proxy support, HTTP/3 and platform caching. Both implement http.Client, so a factory picks one per platform and call sites stay unchanged.

open as a page