Kodokon kodokon.com

Calling an HTTP API: decoding and screen states

Structure a professional network call with the http package: timeout, status codes, typed decoding, and exhaustive states.

11 min · 3 questions

Open this lesson in Kodokon

The http package covers the essentials: requests, headers, body. dio adds interceptors, cancellation, and retry, at the cost of a heavier dependency - a good choice once the app grows beyond a few endpoints. Whatever the client, the structure of a serious network call doesn't change: a timeout, a check of the HTTP status code, a typed decode, and three screen states (loading, error, data) handled explicitly.

BASH
flutter pub add http
DART
import 'dart:convert';

import 'package:http/http.dart' as http;

class Article {
  const Article({required this.title});

  factory Article.fromJson(Map<String, dynamic> json) =>
      Article(title: json['title'] as String);

  final String title;
}

Future<List<Article>> fetchArticles() async {
  final uri = Uri.https('api.example.com', '/articles');
  final response = await http
      .get(uri)
      .timeout(const Duration(seconds: 8));
  if (response.statusCode != 200) {
    throw Exception('HTTP ${response.statusCode}');
  }
  final body = utf8.decode(response.bodyBytes);
  final data = jsonDecode(body) as List<dynamic>;
  return [
    for (final item in data)
      Article.fromJson(item as Map<String, dynamic>),
  ];
}
Timeout, status checked, UTF-8 decode then typing.

Two behaviors often surprise people. One: http throws no exception on a 404 or 500 status - the request "succeeded" from the network's point of view, so it's up to you to check statusCode. Two: jsonDecode returns dynamic; cast immediately (as List<dynamic>, as Map<String, dynamic>) to restore typing. For large payloads, offload the decode to an isolate with compute so you don't freeze the UI.

That leaves representing the screen's state. Boolean flags like isLoading combined with error != null always end up in inconsistent states (loading and error at the same time). A sealed class makes the states mutually exclusive and the switch exhaustive: if you add a state tomorrow, the compiler will point out every screen to update.

DART
import 'package:flutter/material.dart';

sealed class ArticlesState {
  const ArticlesState();
}

class ArticlesLoading extends ArticlesState {
  const ArticlesLoading();
}

class ArticlesError extends ArticlesState {
  const ArticlesError(this.message);

  final String message;
}

class ArticlesLoaded extends ArticlesState {
  const ArticlesLoaded(this.titles);

  final List<String> titles;
}

Widget buildArticlesBody(ArticlesState state) {
  return switch (state) {
    ArticlesLoading() => const Center(
        child: CircularProgressIndicator(),
      ),
    ArticlesError(:final message) => Center(
        child: Text(message),
      ),
    ArticlesLoaded(:final titles) => ListView.builder(
        itemCount: titles.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(titles[index]),
        ),
      ),
  };
}
An exhaustive switch: no state can be forgotten.

Knowledge check

Make sure you remember the key points of this lesson.

  1. Your http.get call receives a 500 response. What does the http package do?
    • It throws an exception you can catch with try/catch.
    • It returns the Response normally: it's up to you to check statusCode.
    • It automatically retries the request three times.
  2. Accented characters display incorrectly in the received data. Most likely cause?
    • jsonDecode doesn't handle UTF-8.
    • Flutter can't display accented characters without a dedicated package.
    • The server omits the charset: response.body is then decoded as latin-1; use utf8.decode(response.bodyBytes).
  3. What is the decisive advantage of a sealed class for modeling a screen's state?
    • The switch becomes exhaustive: the compiler flags any unhandled state.
    • It makes the screen render faster.
    • It lets you modify the state without rebuilding the widget.