Contents
- Configuration and Permissions
- Request Execution and Response Handling
- Background Parsing
- Workflow: Executing Network Operations
- Examples
Configuration and Permissions
Configure the development environment and platform-specific access controls to enable network requests:
- Add the
httpdependency 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.xmlwithin the<manifest>tag:<uses-permission android:name="android.permission.INTERNET" /> - Enable internet clients on macOS by modifying
macos/Runner/DebugProfile.entitlementsandmacos/Runner/Release.entitlementswithin 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.authorizationHeaderkey fromdart:io. - Payload Encoding: When mutating resource states (POST, PUT), encode payload bodies with
jsonEncodefromdart:convert. - Status Validation: Verify response codes. Handle only explicit success status codes (e.g.
200 OKor201 Created). - Throw on Errors: Throw descriptive exceptions when the server responds with unsuccessful status codes. Never return
nullon failure, as this hides issues and results in infinite UI loading spinners. - Client Mocking: Accept an
http.Clientdependency 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:
- [ ] Define model contracts: Create clear, immutable model classes with a custom
fromJsonfactory constructor. - [ ] Establish HTTP clients: Build the network client class accepting
http.Client. - [ ] Formulate requests:
- [ ] For reading (GET): Attach query parameters to the URI.
- [ ] For mutations (POST/PUT): Set
'Content-Type': 'application/json; charset=UTF-8'and attachjsonEncodedata. - [ ] For deletions (DELETE): Return success indicators or empty model mappings upon matching
200 OK.
- [ ] Enforce error handling: Throw meaningful exceptions for non-success status codes.
- [ ] Integrate UI state: Bind network requests to a
FutureBuilderor state management controller in the UI layer. - [ ] Verify boundaries: Test with proper loading screens, error dialogs, and offline-handling feedback loops.
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(),
);
},
),
);
}
}