HTTP / dio
The `package:http` library is the cross-platform HTTP client Flutter ships. It handles JSON APIs, multipart uploads, custom headers, and cancellation. For anything more — interceptors, retry, refresh-token flow — graduate to `dio`. Keep the network layer in a service and let widgets consume domain objects, not Response.
A typed HTTP client, retry, multipart, and cancellation
EXAMPLE
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
// 1) Typed model so the rest of the app does not see JSON
class Order {
final String id; final String customer; final int totalCents; final String status;
Order({required this.id, required this.customer, required this.totalCents, required this.status});
factory Order.fromJson(Map<String, dynamic> j) =>
Order(id: j['id'], customer: j['customer'], totalCents: j['total_cents'], status: j['status']);
}
// 2) A small service with timeout, retries, and structured errors
class ApiClient {
ApiClient({http.Client? client, this.base = 'https://api.example.com'}) :
_c = client ?? http.Client();
final http.Client _c;
final String base;
String? bearer;
Map<String, String> _headers([Map<String, String>? extra]) => {
'accept': 'application/json',
if (bearer != null) 'authorization': 'Bearer $bearer',
...?extra,
};
Future<T> _retry<T>(Future<T> Function() op,
{int max = 3, Duration delay = const Duration(milliseconds: 250)}) async {
var attempt = 0;
while (true) {
try { return await op().timeout(const Duration(seconds: 8)); }
on TimeoutException catch (_) { /* fall through to retry */ }
on SocketException catch (_) { /* fall through to retry */ }
attempt++;
if (attempt >= max) rethrow;
await Future.delayed(delay * (1 << attempt)); // 0.5s, 1s, 2s
}
}
Future<List<Order>> listOrders(String customerId) async {
final uri = Uri.parse('$base/orders').replace(queryParameters: {'customer': customerId});
final res = await _retry(() => _c.get(uri, headers: _headers()));
if (res.statusCode != 200) throw _toError(res);
final list = (jsonDecode(res.body) as List).cast<Map<String, dynamic>>();
return list.map(Order.fromJson).toList();
}
Future<Order> placeOrder(Order order) async {
final uri = Uri.parse('$base/orders');
final res = await _retry(() => _c.post(uri,
headers: _headers({'content-type': 'application/json'}),
body: jsonEncode({'customer': order.customer, 'total_cents': order.totalCents}),
));
if (res.statusCode != 201) throw _toError(res);
return Order.fromJson(jsonDecode(res.body));
}
Future<String> uploadReceipt(File file) async {
final req = http.MultipartRequest('POST', Uri.parse('$base/receipts'))
..headers.addAll(_headers())
..fields['note'] = 'mobile-upload'
..files.add(await http.MultipartFile.fromPath('file', file.path));
final res = await http.Response.fromStream(await req.send());
if (res.statusCode != 201) throw _toError(res);
return jsonDecode(res.body)['url'];
}
Future<void> close() async => _c.close();
ApiError _toError(http.Response r) => ApiError(r.statusCode, r.body);
}
class ApiError implements Exception {
ApiError(this.status, this.body);
final int status; final String body;
@override String toString() => 'ApiError($status): $body';
}
// 3) Use it from a screen — service returns domain objects, widget renders them
final api = ApiClient()..bearer = 'token-here';
Future<void> demo() async {
final orders = await api.listOrders('u1');
for (final o in orders) {
print('${o.id}: ${o.customer} ${o.totalCents}');
}
await api.close();
}
Why it matters
Hide HTTP details behind a service. The rest of the app should never see `http.Response`, `jsonDecode`, or status codes — that boundary is where retries, telemetry, and refresh-token flows belong. Without it, every widget grows its own copy of the same error handling.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import 'package:http/http.dart' as http;
final r = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
final post = jsonDecode(r.body);
Try it Yourself »
Discussion
Loading…