Use package:http to send a request, await the Future<Response>, check the status code, decode the body with jsonDecode and turn the resulting map into a typed model through a fromJson constructor. Kick the request off once (not in build), hand the Future to a FutureBuilder to show loading, error and data states, and move decoding of very large payloads onto a background isolate with compute.
- package:http works on Android, iOS, macOS, Windows, Linux and web; avoid dart:io or dart:html directly. Android needs the INTERNET permission in the manifest and macOS needs the network client entitlement.
- http.get(uri) and http.post(uri, headers:, body:) return Future<http.Response>; await them inside an async function and read response.statusCode and response.body.
- Non-success status codes do not throw. Check them yourself and throw an exception rather than returning null, otherwise FutureBuilder keeps showing the spinner.
- jsonDecode returns dynamic (a Map, List, String, num, bool or null). Cast it and convert to a model class with a fromJson constructor so the rest of the app is typed.
- Create the Future once (initState or a late final field) and pass it to FutureBuilder; a Future created inside build restarts the request on every rebuild.
- Decoding a very large document on the UI isolate causes jank. compute(parseFn, body) runs a top-level function on another isolate and returns the result. For many models, json_serializable generates fromJson/toJson instead of hand-written code.
The widget starts the request once, a repository talks to the server through http.Client, the response is validated and decoded (on a background isolate for big payloads), and the completed Future makes FutureBuilder rebuild.
Mail order with a packing slip in another language
A widget filling in an order form is the request; the postal service (http) carries it and brings a parcel back (the Response). The parcel has a packing slip in a foreign format (JSON text). Before you can shelve the goods you transcribe the slip into your own inventory sheet (fromJson into a model). FutureBuilder is the tracking page: 'in transit' (loading), 'delivered' (data) or 'lost' (error). If the shipment is enormous you do the transcription in the back room so the shop floor stays open (compute on another isolate).
Minimal working example
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class Album {
const Album({required this.id, required this.title});
final int id;
final String title;
factory Album.fromJson(Map<String, dynamic> json) {
return Album(id: json['id'] as int, title: json['title'] as String);
}
}
Future<Album> fetchAlbum() async {
final uri = Uri.parse('https://jsonplaceholder.typicode.com/albums/1');
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('Request failed: ${response.statusCode}');
}
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}
class AlbumPage extends StatefulWidget {
const AlbumPage({super.key});
@override
State<AlbumPage> createState() => _AlbumPageState();
}
class _AlbumPageState extends State<AlbumPage> {
late final Future<Album> _album = fetchAlbum();
@override
Widget build(BuildContext context) {
return FutureBuilder<Album>(
future: _album,
builder: (context, snapshot) {
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
if (!snapshot.hasData) return const CircularProgressIndicator();
return Text(snapshot.data!.title);
},
);
}
}- L10fromJson is the one place that knows the JSON shape; the casts throw a TypeError if the server changes a field's type.
- L17http.get returns a Future<Response>; await suspends this function without blocking the UI isolate.
- L18A 404 or 500 is a normal Response, not an exception. Throwing here makes FutureBuilder report hasError instead of spinning forever.
- L21jsonDecode returns dynamic; cast to Map<String, dynamic> for an object or List<dynamic> for an array.
- L32A late final field initialises on first use and only once, so rebuilds reuse the same Future. initState works equally well.
- L36FutureBuilder subscribes to the Future and rebuilds its builder when it completes.
- L39Check hasError before hasData: a failed Future has no data, and a null-returning Future never sets hasData.
Use cases
- Use http.get plus a fromJson model for straightforward REST reads, and FutureBuilder (or a ChangeNotifier ViewModel) to render loading, error and data states.
- Use http.post with a JSON-encoded body and Content-Type header to create or update resources; expect 201 for creation.
- Use compute when decoding takes long enough to drop frames (large lists, deeply nested documents) and the parse function is top-level.
- Use json_serializable when the project has many models or nested objects, so field names and types are checked by generated code instead of hand-written casts.
Benefits
- One API across all six platforms; the http package abstracts dart:io and the browser's fetch.
- Typed models catch typos and type mismatches at the boundary, so widgets never touch raw maps.
- FutureBuilder and ChangeNotifier-based ViewModels make the three UI states explicit instead of implicit nulls.
Limitations
- The http package has no interceptors, retries or caching built in; those are left to you or to a package such as dio.
- Hand-written fromJson scales poorly: a renamed server field is a runtime error, not a compile error, unless you adopt code generation.
- compute copies the message to and from the isolate and on the web runs on the main thread, so it pays off only for genuinely heavy parsing.
Alternatives and trade-offs
- dio — You need interceptors, request cancellation, form data or built-in retry/logging on top of HTTP.
- json_serializable or freezed — There are many or nested models and you want generated fromJson/toJson with compile-time field checks.
- StreamBuilder with WebSockets or server-sent events — Data changes continuously and polling with GET would be wasteful.
- A ChangeNotifier ViewModel instead of FutureBuilder — The screen needs refresh, retry or several requests, and a single Future no longer describes its state.
Starting the request inside build
Problem: build runs whenever anything above rebuilds. A Future created there fires a new HTTP request every time, and FutureBuilder restarts from the loading state because it received a different Future.
Fix: Create the Future once in initState (or as a late final field) and store it; pass that same object to FutureBuilder.
@override
Widget build(BuildContext context) {
return FutureBuilder<Album>(
future: fetchAlbum(), // re-requests on every build
builder: (context, snapshot) => Text('${snapshot.data?.title}'),
);
}late Future<Album> _album;
@override
void initState() {
super.initState();
_album = fetchAlbum();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Album>(
future: _album,
builder: (context, snapshot) => Text('${snapshot.data?.title}'),
);
}Returning null instead of throwing on a bad status code
Problem: snapshot.hasData is only true for a non-null value. A fetch that returns null on 404 completes 'successfully' with nothing, so the FutureBuilder shows the loading spinner forever and the user never sees an error.
Fix: Throw on any status you do not expect. The exception surfaces as snapshot.hasError, which the builder can render.
Future<Album?> fetchAlbum() async {
final response = await http.get(_uri);
if (response.statusCode != 200) return null; // spinner forever
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}Future<Album> fetchAlbum() async {
final response = await http.get(_uri);
if (response.statusCode != 200) {
throw Exception('Failed to load album: ${response.statusCode}');
}
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
}Passing the wrong thing to compute
Problem: compute serialises its message to another isolate. Passing an http.Response, a Future, a closure that captures a widget, or an instance method causes an error, and the parse function silently runs nowhere.
Fix: Send plain data (the response body String) and point compute at a top-level or static function that returns plain data (a List of simple objects).
Forgetting platform permissions
Problem: Requests that work on iOS and desktop fail on an Android release build with a SocketException, or on macOS with a sandbox denial, because the platform never granted network access.
Fix: Add <uses-permission android:name="android.permission.INTERNET" /> to AndroidManifest.xml and the com.apple.security.network.client entitlement to the macOS .entitlements files.
This page is Boring Doc’s independent explanation. For the complete specification use the official pages below (content © The Flutter Authors, CC BY 3.0; code samples BSD). Dates show when the explanation was last checked and when the official page last changed.
- https://docs.flutter.dev/data-and-backend/networking
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/networking/fetch-data
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/networking/send-data
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/cookbook/networking/background-parsing
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- https://docs.flutter.dev/data-and-backend/serialization/json
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- Fetch data from the internet (learning pathway) Flutter 3.47.7 Explanation verified against current sourcehttps://docs.flutter.dev/learn/pathway/tutorial/http-requests
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- FutureBuilder class API reference Flutter 3.47.7https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
- Last checked
- Oct 21, 2026
AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.