Nearly every app talks to a backend, and typed models catch API mismatches early.
Add the official `http` package (flutter pub add http) and call http.get(Uri.parse(url)), http.post(uri, headers: ..., body: jsonEncode(data)). Each returns a Future<Response> with statusCode and body. Check the status yourself; http doesn't throw on 404 or 500. Network failures throw (e.g. SocketException, or ClientException).
Decode with jsonDecode(response.body) from dart:convert. The result is untyped (Map<String, dynamic> or List<dynamic>), so convert it to model classes at the boundary:
- A
factory User.fromJson(Map<String, dynamic> json)constructor, ideally using Dart 3 map patterns to check types:{'id': int id, 'name': String name}. - A
toJson()method for sending.
For larger models, code generation with json_serializable (plus build_runner) or freezed writes this boilerplate for you.
Good practices:
- Keep HTTP code out of widgets, in a repository or service class that returns models and throws domain errors.
- Use a single
http.Clientand close it when done; it reuses connections. - Add timeouts:
.timeout(const Duration(seconds: 10)). - Decode very large JSON off the UI thread with
computeorIsolate.run. - On Android, release builds need the
INTERNETpermission in the manifest; macOS apps need the network client entitlement.
dio is a popular alternative with interceptors, cancellation and progress callbacks.
Example
import 'dart:convert';
import 'package:http/http.dart' as http;
class Todo {
const Todo({required this.id, required this.title, required this.done});
final int id;
final String title;
final bool done;
factory Todo.fromJson(Map<String, dynamic> json) => switch (json) {
{'id': int id, 'title': String title, 'completed': bool done} =>
Todo(id: id, title: title, done: done),
_ => throw FormatException('Bad todo JSON: $json'),
};
}
Future<List<Todo>> fetchTodos(http.Client client) async {
final uri = Uri.parse('https://jsonplaceholder.typicode.com/todos?_limit=5');
final res = await client.get(uri).timeout(const Duration(seconds: 10));
if (res.statusCode != 200) throw Exception('HTTP ${res.statusCode}');
final list = jsonDecode(res.body) as List<dynamic>;
return [for (final item in list) Todo.fromJson(item as Map<String, dynamic>)];
}Remember
- Check
statusCodeyourself — http doesn't throw on 4xx/5xx - Convert JSON to typed models at the boundary, e.g. with map patterns
- Keep HTTP in a service/repository, not in widgets
Watch out for
- Using
json['field']dynamically throughout the UI, so API changes crash at run time far from the cause.