Structure a professional network call with the http package: timeout, status codes, typed decoding, and exhaustive states.
Open this lesson in KodokonThe 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.
flutter pub add httpimport '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>),
];
}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.
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]),
),
),
};
}http.get call receives a 500 response. What does the http package do?try/catch.Response normally: it's up to you to check statusCode.jsonDecode doesn't handle UTF-8.response.body is then decoded as latin-1; use utf8.decode(response.bodyBytes).sealed class for modeling a screen's state?switch becomes exhaustive: the compiler flags any unhandled state.