Irbisa · Cheatsheet

Networking & REST

http, Dio, REST APIs, JSON serialization, interceptors

Middle Developer · 25 questions

  1. 01

    Which HTTP methods and status codes should you know, and how should the app react to 401, 403, 404 and 5xx?

    GET reads, POST creates, PUT replaces, PATCH changes part, DELETE removes; 2xx is success, 3xx redirect, 4xx the client's mistake, 5xx the server's.

  2. 02

    How do you load data from an API when a screen opens and show loading, error and data states?

    Start the request once (in initState or in your state-management layer), keep the Future in a field, and let FutureBuilder render the three states from its snapshot.

    class _UserScreenState extends State<UserScreen> {  late Future<User> _user;   @override  void initState() {    super.initState();…
  3. 03

    This fetchUser works in the demo and crashes in production as soon as a user is deleted: what does it get wrong?

    Future<User> fetchUser(int id) async {  final res = await http.get(Uri.parse('$baseUrl/users/$id'));  final json = jsonDecode(res.body) as Map<String, dynamic>;  return User.fromJson(json);}

    package:http does not throw on 4xx or 5xx; the code decodes whatever body came back and feeds it to User.fromJson.

  4. 04

    Search returns nonsense as soon as someone types "C# & Java" into the box: what is wrong with the way that request URL is built?

    The term is interpolated into a URL string, so # starts a fragment and everything after it, & included, never reaches the server; it sees only q=C.

    // wrong: '#' starts a fragment, so the server sees only q=Cfinal bad = Uri.parse('https://api.example.com/search?q=$term'); // right: every value is percent-encodedfinal good = Uri.https('api.example.com', '/search', {'q': term, 'page': '1'});…
  5. 05

    Which HTTP client and JSON-serialization approach would you pick for a Flutter app, and why?

    Past a handful of endpoints, dio: interceptors, cancellation, multipart and a base URL come built in.

    part 'user.g.dart'; @JsonSerializable()class User {  const User({required this.id, required this.name, required this.email});…
  6. 06

    Find the bug: fetchOrders fails on every call, although the server returns valid JSON with the expected fields.

    class Order {  Order({required this.id, required this.total});  final int id;  final double total;   factory Order.fromJson(Map<String, dynamic> json) =>      Order(id: json['id'] as int, total: (json['total'] as num).toDouble());} Future<List<Order>> fetchOrders(Dio dio) async {  final res = await dio.get<Map<String, dynamic>>('/orders');  final items = res.data!['items'] as List<Map<String, dynamic>>;  return items.map(Order.fromJson).toList();}

    jsonDecode builds every JSON array as a List<dynamic>, so as List<Map<String, dynamic>> throws type 'List<dynamic>' is not a subtype of ... at runtime on every response, however valid the JSON is.

  7. 07

    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?

    Set all three: connectTimeout for opening the connection, sendTimeout for writing the body and receiveTimeout for the response.

    final dio = Dio(BaseOptions(  baseUrl: 'https://api.example.com',  connectTimeout: const Duration(seconds: 8), // TCP + TLS handshake  sendTimeout: const Duration(seconds: 15), // writing the request body  receiveTimeout: const Duration(seconds: 20), // gap between response bytes));…
  8. 08

    How do you implement request/response interceptors in Flutter?

    In Dio, extend Interceptor and override onRequest, onResponse, onError; each calls handler.next, resolve or reject.

    class AuthInterceptor extends Interceptor {  AuthInterceptor(this._tokens);  final TokenStore _tokens;   @override  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {…
  9. 09

    Logged-out users say the public catalog spins forever, while signed-in users are fine: what is wrong with this interceptor?

    dio.interceptors.add(InterceptorsWrapper(  onRequest: (options, handler) async {    final token = await storage.read(key: 'access_token');    if (token != null) {      options.headers['Authorization'] = 'Bearer $token';      handler.next(options);    }  },));

    When there is no token, the callback never calls handler.next, so the request stops inside the interceptor chain and its future never completes.

  10. 10

    How do you map DioException types into typed domain errors?

    Map once, in one place, so HTTP never leaks into business logic: a DioException becomes a typed failure your repositories and view models can handle without knowing what a status code is.

    sealed class AppException implements Exception {  const AppException();} class NetworkException extends AppException { const NetworkException(); }class UnauthorizedException extends AppException { const UnauthorizedException(); }…
  11. 11

    The user leaves a screen while its request is still in flight. How do you cancel it?

    Give the screen its own CancelToken, pass it to every request the screen starts, and call token.cancel() from dispose.

    class OrdersController {  OrdersController(this._dio);  final Dio _dio;  final _cancel = CancelToken(); // one per screen   Future<List<Order>?> load() async {…
  12. 12

    Find the bug: after fast typing, the search list sometimes shows results that do not match the text in the box.

    class _SearchState extends State<SearchScreen> {  List<Product> _results = [];   Future<void> _onQueryChanged(String query) async {    final results = await widget.api.search(query);    if (!mounted) return;    setState(() => _results = results);  }   @override  Widget build(BuildContext context) => Column(children: [        TextField(onChanged: _onQueryChanged),        Expanded(child: ProductList(_results)),      ]);}

    Every keystroke starts a request, and whichever response arrives last wins: if the response for "p" is slower than the one for "phone", it overwrites the newer results and the list no longer matches the box.

  13. 13

    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?

    A new client means a new connection pool, so every call repeats the DNS lookup and the TCP and TLS handshakes instead of reusing a socket that is already open.

  14. 14

    One call in twenty fails on a flaky mobile network: how do you add retries without hammering the server or charging a user twice?

    Retry only what is safe to repeat, back off exponentially with jitter, and let the server's Retry-After override your own schedule.

    class RetryInterceptor extends Interceptor {  RetryInterceptor(this._dio, {this.maxAttempts = 3});  final Dio _dio;  final int maxAttempts;  @override  Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {…
  15. 15

    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?

    All 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 RefreshInterceptor extends Interceptor {  RefreshInterceptor(this._dio, this._authDio, this._tokens);  final Dio _dio, _authDio; // _authDio has no auth interceptor: no re-entry  final TokenStore _tokens;  Future<String>? _refreshing;  @override…
  16. 16

    How do you upload files (multipart/form-data) in Flutter?

    Wrap each file in a MultipartFile, put the fields and files into FormData, and post that.

    Future<void> uploadAvatar(  Dio dio,  String path,  CancelToken cancel,  ValueNotifier<double> progress,) async {…
  17. 17

    How do you download a large file with a progress bar and resume it after the connection drops?

    Stream the response into a temporary file, report progress from the received-bytes callback, and resume by asking the server for the remaining byte range.

    Future<void> downloadResumable(    Dio dio, String url, String path, void Function(double) onProgress) async {  final part = File('$path.part');  final have = await part.exists() ? await part.length() : 0;   final res = await dio.get<ResponseBody>(…
  18. 18

    Offset or cursor pagination: which do you ask the backend for, and when should the client request the next page?

    Ask for cursor pagination: the server returns an opaque nextCursor and the client sends it back, so rows inserted or deleted between pages cannot repeat or skip items.

  19. 19

    How do you cache HTTP responses in a Flutter app?

    Three layers, each right for a different case: HTTP cache headers, an app-level cache of parsed models, and an image cache.

  20. 20

    How do you detect that the device went offline, and why is connectivity_plus alone not enough?

    connectivity_plus tells you which network interface is up, not whether the internet is reachable: Wi-Fi behind a captive portal reports wifi.

    class Reachability {  Reachability(this._dio);  final Dio _dio;   // connectivity_plus 6+ emits a list: Wi-Fi and VPN can be up together.  Stream<bool> get hasInterface => Connectivity()…
  21. 21

    When is GraphQL worth adding to a Flutter app?

    GraphQL is one endpoint where the client describes exactly the data it wants, so a screen gets no more and no less.

  22. 22

    How do you connect to a WebSocket and handle reconnection?

    Open the channel with web_socket_channel, assume it will drop, and reconnect with exponential backoff plus jitter; add a ping so a dead socket is noticed, and reopen it when the app resumes.

    class LiveFeed {  LiveFeed(this.uri);  final Uri uri;  final events = StreamController<String>.broadcast();  WebSocketChannel? _channel;  var _attempt = 0, _closed = false;…
  23. 23

    The chat endpoint streams tokens as server-sent events over Dio and the stream dies after ten seconds: what is going on, and how do you parse it?

    receiveTimeout measures the gap between byte events, not the length of the response, so a stream that stays silent longer than the timeout, for example while the model thinks, trips it.

    Stream<String> chatEvents(Dio dio, Object body, CancelToken cancel) async* {  final res = await dio.post<ResponseBody>(    '/chat/stream',    data: body,    cancelToken: cancel,    options: Options(…
  24. 24

    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?

    Bound how many requests are in flight and collect a per-item outcome, so the pool is never flooded and one failure cannot throw away 199 good results.

    Future<List<Item?>> loadDetails(    ItemApi api, List<int> ids, CancelToken cancel) async {  final pool = Pool(6); // at most six requests in flight  final futures = [    for (final id in ids)      pool.withResource(() {…
  25. 25

    The backend adds a field, renames an enum value and makes a string nullable. How does your app survive that?

    Treat the payload as untrusted input, and make the data layer the only place where a contract change is allowed to fail.

    enum OrderStatus { pending, shipped, delivered, unknown } @JsonSerializable()class OrderDto {  OrderDto({required this.id, required this.status, this.note});…