| name | flutter-use-http-package |
| description | Perform REST API networking operations (GET, POST, PUT, DELETE) using the lightweight and robust standard `http` package, including platform configurations and background parsing models. |
| metadata | {"platforms":"flutter","languages":"dart","category":"networking"} |
Contents
Configuration and Permissions
Configure the development environment and platform-specific access controls to enable network requests:
- Add the
http dependency using your terminal:
flutter pub add http
- Import the library with an alias in your Dart files:
import 'package:http/http.dart' as http;
- Enable internet permissions on Android by modifying
android/app/src/main/AndroidManifest.xml within the <manifest> tag:
<uses-permission android:name="android.permission.INTERNET" />
- Enable internet clients on macOS by modifying
macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements within the <dict> tag:
<key>com.apple.security.network.client</key>
<true/>
Request Execution and Response Handling
Design robust REST clients by applying these best practices:
- Strict URL Parsing: Always parse endpoint strings via
Uri.parse('url'). Never pass raw strings to client calls.
- Headers and Authentication: Attach all authorization, accept, and content-type configurations. Inject access tokens using the
HttpHeaders.authorizationHeader key from dart:io.
- Payload Encoding: When mutating resource states (POST, PUT), encode payload bodies with
jsonEncode from dart:convert.
- Status Validation: Verify response codes. Handle only explicit success status codes (e.g.
200 OK or 201 Created).
- Throw on Errors: Throw descriptive exceptions when the server responds with unsuccessful status codes. Never return
null on failure, as this hides issues and results in infinite UI loading spinners.
- Client Mocking: Accept an
http.Client dependency in your network classes instead of calling standard global methods. This facilitates easy testing and mock injection.
Background Parsing
Offload JSON decoding and mapping to a background thread to prevent UI jank (dropped frames) when handling payloads larger than 1MB:
- Import
package:flutter/foundation.dart.
- Run the parsing logic within the
compute() function to spawn a background isolate.
- Ensure the parsing function is defined as a top-level function or static class method. Closures and standard instance methods cannot cross isolate boundaries.
Workflow: Executing Network Operations
Follow this checklist to build and verify network integration:
Examples
Complete Network Client and Isolate Parser
This example demonstrates setting up a robust, testable network client that parses complex payload lists in a background isolate.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
// 1. Top-level parsing function (required to run in a separate Isolate)
List<Product> parseProducts(String responseBody) {
final parsed = (jsonDecode(responseBody) as List<dynamic>).cast<Map<String, dynamic>>();
return parsed.map<Product>((json) => Product.fromJson(json)).toList();
}
// 2. Service Layer exposing testable methods
class ProductService {
final http.Client client;
const ProductService({required this.client});
Future<List<Product>> fetchProducts() async {
final response = await client.get(
Uri.parse('https://api.example.com/products'),
headers: {
HttpHeaders.acceptHeader: 'application/json',
HttpHeaders.authorizationHeader: 'Bearer token_here',
},
);
if (response.statusCode == 200) {
// Offload heavy JSON parsing of larger lists to a background isolate
return compute(parseProducts, response.body);
} else {
throw HttpException('Failed to load products. Status: ${response.statusCode}');
}
}
}
// 3. Immutable Data Model
class Product {
final int id;
final String name;
final double price;
const Product({
required this.id,
required this.name,
required this.price,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
);
}
}
// 4. UI Layer Integration
class ProductListView extends StatefulWidget {
final ProductService productService;
const ProductListView({
super.key,
required this.productService,
});
@override
State<ProductListView> createState() => _ProductListViewState();
}
class _ProductListViewState extends State<ProductListView> {
late Future<List<Product>> _futureProducts;
@override
void initState() {
super.initState();
// Cache the future once to prevent redundant re-fetching on rebuilds
_futureProducts = widget.productService.fetchProducts();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Store Products'),
),
body: FutureBuilder<List<Product>>(
future: _futureProducts,
builder: (context, snapshot) {
if (snapshot.hasData) {
final products = snapshot.data!;
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
trailing: Text('\$${product.price.toStringAsFixed(2)}'),
);
},
);
} else if (snapshot.hasError) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'An error occurred: ${snapshot.error}',
textAlign: TextAlign.center,
),
),
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
),
);
}
}