Networking & REST
http, Dio, REST APIs, JSON serialization, interceptors
- 01
Which HTTP client and JSON-serialization approach would you pick for a Flutter app, and why?
MediumPast a handful of endpoints the default client is
dio, because interceptors, cancellation, multipart and base-URL handling come built in.// ── Model with json_serializable ────────────────── @JsonSerializable() class User { final int id; final String name; @JsonKey(name: 'email_address') … - 02
How do you implement pagination and infinite scroll in Flutter?
MediumInfinite scroll is a paging cursor plus a scroll trigger that fires once, near the bottom of the list.
class ProductListCubit extends Cubit<ProductListState> { final ProductRepository _repo; static const _pageSize = 20; int _page = 1; bool _hasMore = true; … - 03
How do you implement request/response interceptors in Flutter?
MediumAn interceptor runs on every request, response and error, so cross-cutting concerns live in one place instead of at every call site.
// ── Using Dio with interceptors ──────────── import 'package:dio/dio.dart'; class ApiClient { late final Dio dio; … - 04
How do you upload files (multipart/form-data) in Flutter?
MediumFile uploads use multipart/form-data, which packages each field — including binary file content — into a single request body.
import 'package:dio/dio.dart'; import 'package:image_picker/image_picker.dart'; final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com')); // ── Pick + upload an image with progress ── … - 05
How do you cache HTTP responses in a Flutter app?
MediumCaching cuts network traffic, makes repeat views feel instant, and buys you limited offline use.
// ── 1) HTTP cache with dio_cache_interceptor ── import 'package:dio/dio.dart'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; import 'package:http_cache_hive_store/http_cache_hive_store.dart'; // hive_ce-backed store Future<Dio> buildCachedDio() async { … - 06
How do you connect to a WebSocket and handle reconnection?
MediumWebSockets give you a long-lived bidirectional channel — push from server, send from client, no polling.
import 'dart:async'; import 'package:web_socket_channel/web_socket_channel.dart'; enum WsState { disconnected, connecting, connected } class ChatClient { … - 07
How do you map DioException types into typed domain errors?
MediumCatching
DioExceptioneverywhere bleeds the HTTP layer into your business logic.// ── Domain hierarchy ── sealed class AppError implements Exception { final String message; const AppError(this.message); } class NetworkError extends AppError { const NetworkError([super.m = 'No network']); } class TimeoutError extends AppError { const TimeoutError([super.m = 'Timeout']); } class ServerError extends AppError { final int statusCode; const ServerError(this.statusCode, [super.m = 'Server error']); } class BadRequestError extends AppError { … - 08
How do you design an offline-first sync engine for a Flutter app?
HardOffline-first means the app works without a network and syncs in the background.
// ── Tables ── // notes (id, body, updated_at, server_id, dirty) // pending_ops (id, op, table, row_id, payload, attempts, last_error, idempotency_key) import 'package:uuid/uuid.dart'; final _uuid = const Uuid(); … - 09
When and how do you use GraphQL in a Flutter app?
MediumGraphQL is a single endpoint where the client describes the data it wants in a typed query language.
// pubspec: graphql_flutter: ^5.x.x import 'package:graphql_flutter/graphql_flutter.dart'; Future<GraphQLClient> buildClient() async { final HttpLink httpLink = HttpLink('https://api.example.com/graphql'); … - 10
The user leaves a screen while its request is still in flight. How do you cancel it?
EasyGive the screen its own CancelToken, pass it to every request the screen starts, and cancel it from dispose.
class _FeedPageState extends State<FeedPage> { final _cancel = CancelToken(); Future<void> _load() async { try { final res = await dio.get('/feed', cancelToken: _cancel); … - 11
How do you download a large file with a progress bar and resume it after the connection drops?
MediumStream the response straight to a temporary file, report progress from the received-bytes callback, and resume by asking the server for the remaining byte range.
Future<File> downloadWithResume(String url, String finalPath) async { final part = File('$finalPath.part'); final already = await part.exists() ? await part.length() : 0; final res = await dio.get<ResponseBody>( url, … - 12
The backend adds a field, renames an enum value and makes a string nullable. How does your app survive that?
HardTreat the payload as untrusted input and make the data layer the only place where a contract change is allowed to fail.
// ── Typed readers instead of blind casts ── String str(Object? v, {String fallback = ''}) => v is String ? v : fallback; int? intOrNull(Object? v) => v is int ? v : (v is num ? v.toInt() : (v is String ? int.tryParse(v) : null)); enum OrderStatus { pending, shipped, delivered, unknown } … - 13
The app sits on a spinner for two minutes on a bad connection before anything fails — which Dio timeouts fix that, and what does each one cover?
EasyDio sets no timeouts at all by default, so a stalled request lives as long as the OS socket lets it.
final dio = Dio(BaseOptions( baseUrl: 'https://api.example.com', connectTimeout: const Duration(seconds: 8), // TCP + TLS handshake sendTimeout: const Duration(seconds: 15), // only requests with a body receiveTimeout: const Duration(seconds: 20), // gap between byte events )); … - 14
Search returns nonsense as soon as someone types "C# & Java" into the box — what is wrong with the way that request URL is built?
EasyThe term is being interpolated into a URL string, so
#starts a fragment and&starts a new parameter — the server never sees what the user typed.const term = 'C# & Java'; // ❌ interpolation: '#' truncates the query, '&' invents a parameter final bad = Uri.parse('https://api.example.com/search?q=$term'); // https://api.example.com/search?q=C#%20&%20Java -> the server sees q=C … - 15
One call in twenty fails on a flaky mobile network — how do you add retries without hammering the server or charging a user twice?
MediumRetry only what is safe to repeat, back off exponentially with jitter, and let the server's
Retry-Afteroverride your own schedule.class RetryInterceptor extends Interceptor { RetryInterceptor(this._dio, {this.maxAttempts = 3}); final Dio _dio; final int maxAttempts; final _rand = Random(); … - 16
A 3 MB JSON list lands and the app drops frames for half a second — what has Dio already moved off the main isolate, and what is still yours to move?
MediumDio already decodes a large JSON body in a background isolate; the jank you are left with is your own
fromJsonmapping running on the UI isolate.// ❌ Dio decoded off-thread, then you map 10k maps on the UI isolate final res = await dio.get('/feed'); final posts = (res.data as List) .map((j) => Post.fromJson(j as Map<String, dynamic>)) .toList(); … - 17
Five screens fire requests at once with an expired access token, and the user lands back on the login screen — what did the refresh interceptor get wrong?
MediumAll five 401s started their own refresh: the first one rotated the refresh token, and the other four presented a token the server had just revoked.
class AuthInterceptor extends Interceptor { AuthInterceptor(this._dio, this._store); final Dio _dio; // the app client, used to replay final TokenStore _store; final Dio _plain = Dio(BaseOptions(baseUrl: 'https://api.example.com')); … - 18
A teammate builds a fresh Dio inside every repository method and latency is up a couple of hundred milliseconds per call — what is the app paying for?
MediumA new client means a new connection pool, so every call repeats the DNS lookup, the TCP handshake and the TLS handshake instead of reusing a socket that is already open.
class UserRepo { // ❌ a new pool, new handshakes, no keep-alive — on every single call Future<User> load(String id) async { final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com')); return User.fromJson((await dio.get('/users/$id')).data); } … - 19
A screen needs details for 200 items, and Future.wait over 200 requests either trips the rate limit or loses 199 good results to one failure — what do you do instead?
HardBound how many requests are in flight and collect a per-item outcome, so the pool is never flooded and one failure cannot discard the successes.
// ❌ 200 requests at once, and one failure loses all 200 results final all = await Future.wait(ids.map(api.detail)); // ✅ bounded fan-out that keeps a per-item outcome typedef Outcome = ({String id, Detail? value, Object? error}); … - 20
The chat endpoint streams tokens as server-sent events, you wire it up with Dio, and the stream dies after ten seconds — what is going on and how do you parse it properly?
HardreceiveTimeoutmeasures the gap between byte events, not the length of the response, so a stream that idles between heartbeats trips it — disable it for this request and parse the body line by line instead of chunk by chunk.class SseEvent { const SseEvent(this.event, this.data, this.id); final String event; final String data; final String? id; } …