Flutter: Networking and REST API
An app without networking is an island — API requests are the bridge to the mainland.
📋 Prerequisites: You need to master the following first
- Lesson 10: List and Scrolling
1. What You Will Learn
httppackage basic GET/POST/PUT/DELETE requestsDiodeep-dive: interceptors, CancelToken, FormData upload, timeout retry- JSON serialization:
json_serializable+build_runnercode generation vs manual parsing - Error handling strategy: network exceptions / 401 auth / 500 server errors unified interception
- ShopApp Dio Repository layer, integrating with
/api/v1/productsproduct endpoint
2. A True Story of Network Request Failures
(1) Pain Point: Network Errors Crash the App
On ShopApp's launch day, users on unstable connections got white-screen crashes — no try-catch, no timeout, no retry. Worse, when the Token expired, all requests returned 401 and the app had no auto-refresh mechanism, forcing users to log in again. 2 thousand negative reviews in one day.
(2) The Dio Interceptor Solution
Dio's interceptor mechanism can uniformly attach tokens before requests go out, automatically handle 401 responses by refreshing tokens, and retry on network timeouts.
import 'package:dio/dio.dart';
// ⚙️ Install dependency: flutter pub add dio
// Assume token and refreshToken are defined elsewhere
// String token = '...';
// Future<String> refreshToken() async { ... }
// Auth interceptor: auto-attach token + auto-refresh on 401
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
token = await refreshToken();
return handler.resolve(await dio.fetch(error.requestOptions));
}
handler.next(error);
},
));
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
(3) Benefit: Zero Network-Related Crashes
After using Dio interceptors, Bob gets auto 401 token refresh, 3x timeout retry, and unified error display. Network-related crash rate drops from 5% to 0.01%.
3. Basic HTTP Requests
(1) http Package CRUD Operations
| Method | HTTP Verb | Purpose |
|---|---|---|
http.get |
GET | Retrieve resources |
http.post |
POST | Create resources |
http.put |
PUT | Update resources |
http.delete |
DELETE | Delete resources |
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: http package basic requests
import 'package:http/http.dart' as http;
import 'dart:convert';
// ⚙️ Install dependency: flutter pub add http
// Custom class definition sources:
// - Product: see Lesson 5 json_serializable model in this lesson
// - ApiException: see Lesson 6 unified error handling in this lesson
// Assume token is defined elsewhere
// String token = '...';
class ProductApi {
static const String baseUrl = 'https://api.shopapp.com/v1';
Future<List<Product>> getProducts({int page = 1}) async {
final response = await http.get(
Uri.parse('$baseUrl/products?page=$page'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return (data['items'] as List).map((j) => Product.fromJson(j)).toList();
}
throw ApiException(response.statusCode, response.body);
}
Future<Product> createProduct(Product product) async {
final response = await http.post(
Uri.parse('$baseUrl/products'),
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $token'},
body: jsonEncode(product.toJson()),
);
if (response.statusCode == 201) {
return Product.fromJson(jsonDecode(response.body));
}
throw ApiException(response.statusCode, response.body);
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
4. Dio Deep-Dive
(1) http vs Dio Comparison
| Feature | http | Dio |
|---|---|---|
| Basic CRUD | ✅ | ✅ |
| Interceptors | ❌ | ✅ |
| CancelToken | ❌ | ✅ |
| FormData upload | Manual | ✅ |
| Timeout retry | Manual | ✅ |
| Request logging | Manual | ✅ (dio_logger) |
| Global config | ❌ | ✅ BaseOptions |
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: Dio basic configuration
import 'package:dio/dio.dart';
// ⚙️ Install dependency: flutter pub add dio
// Custom class definition sources:
// - Product: see Lesson 5 json_serializable model in this lesson
// - Order/Cart: see Lesson 14 Phase 2 practice
final dio = Dio(BaseOptions(
baseUrl: 'https://api.shopapp.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
// GET request
Future<List<Product>> getProducts({int page = 1}) async {
final response = await dio.get('/products', queryParameters: {'page': page});
return (response.data['items'] as List).map((j) => Product.fromJson(j)).toList();
}
// POST request
Future<Order> createOrder(Cart cart) async {
final response = await dio.post('/orders', data: cart.toJson());
return Order.fromJson(response.data);
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: Auth interceptor + Token refresh
import 'package:dio/dio.dart';
// Custom class definition sources:
// - SecureStorage: see Lesson 13 Flutter Secure Storage
// - AuthService: see Lesson 14 Auth Notifier
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = SecureStorage.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
try {
final newToken = await _refreshToken();
SecureStorage.saveToken(newToken);
// Retry original request with new token
err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
final response = await dio.fetch(err.requestOptions);
return handler.resolve(response);
} catch (_) {
// Refresh failed, redirect to login
AuthService.logout();
}
}
handler.next(err);
}
Future<String> _refreshToken() async {
final refreshToken = SecureStorage.getRefreshToken();
final response = await Dio().post(
'https://api.shopapp.com/v1/auth/refresh',
data: {'refresh_token': refreshToken},
);
return response.data['access_token'];
}
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: CancelToken request cancellation
import 'package:dio/dio.dart';
// ⚙️ Install dependency: flutter pub add dio
CancelToken? _cancelToken;
void searchProducts(String query) {
_cancelToken?.cancel('New search request');
_cancelToken = CancelToken();
dio.get('/products/search', queryParameters: {'q': query},
cancelToken: _cancelToken);
}
// Cancel on dispose
@override
void dispose() {
_cancelToken?.cancel();
super.dispose();
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
5. JSON Serialization
(1) Serialization Method Comparison
| Method | Pros | Cons | Use Case |
|---|---|---|---|
| Manual fromJson/toJson | No dependencies, full control | Repetitive, error-prone | Few simple models |
| json_serializable | Type-safe, auto-generated | Requires build_runner | Many models |
| freezed | Immutable + copyWith + union | More dependencies | Complex domain models |
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: json_serializable model
import 'package:json_annotation/json_annotation.dart';
// ⚙️ Install dependency: flutter pub add json_annotation
// ⚙️ Dev dependency: flutter pub add --dev json_serializable build_runner
part 'product.g.dart';
@JsonSerializable()
class Product {
final int id;
final String name;
@JsonKey(name: 'unit_price')
final double price;
@JsonKey(name: 'image_url')
final String? imageUrl;
final double rating;
const Product({
required this.id,
required this.name,
required this.price,
this.imageUrl,
this.rating = 0.0,
});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
// Run: dart run build_runner build --delete-conflicting-outputs
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
6. Unified Error Handling
▶ Example
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
: ApiException + error interceptor
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
// ⚙️ Install dependency: flutter pub add dio
class ApiException implements Exception {
final int? statusCode;
final String message;
final String? errorCode;
ApiException(this.statusCode, this.message, {this.errorCode});
factory ApiException.fromDioError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return ApiException(null, 'Connection timeout. Please try again.');
case DioExceptionType.connectionError:
return ApiException(null, 'No internet connection.');
case DioExceptionType.badResponse:
final status = error.response?.statusCode;
final msg = error.response?.data['message'] ?? 'Server error';
if (status == 401) return ApiException(401, 'Session expired. Please login again.');
if (status == 403) return ApiException(403, 'Access denied.');
if (status == 404) return ApiException(404, 'Resource not found.');
if (status == 422) return ApiException(422, msg, errorCode: error.response?.data['code']);
if (status != null && status >= 500) return ApiException(status, 'Server error. Please try later.');
return ApiException(status, msg);
default:
return ApiException(null, 'Unexpected error.');
}
}
}
// Usage in UI
try {
final products = await productRepo.getProducts();
} on ApiException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message)),
);
}
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
7. Complete Example: ShopApp Dio Repository
import 'package:dio/dio.dart';
// ⚙️ Install dependency: flutter pub add dio
// Custom class definition sources:
// - Product: see Lesson 5 json_serializable model in this lesson
// - CartItem/Address/Order: see Lesson 14 Phase 2 practice
// - AuthInterceptor: see Lesson 4 Auth interceptor in this lesson
class ProductRepository {
final Dio _dio;
ProductRepository(this._dio);
Future<List<Product>> getProducts({
int page = 1,
int limit = 20,
String? category,
String? search,
}) async {
final params = {'page': page, 'limit': limit};
if (category != null) params['category'] = category;
if (search != null) params['q'] = search;
final response = await _dio.get('/products', queryParameters: params);
return (response.data['items'] as List)
.map((j) => Product.fromJson(j))
.toList();
}
Future<Product> getProductById(int id) async {
final response = await _dio.get('/products/$id');
return Product.fromJson(response.data);
}
Future<Order> createOrder(List<CartItem> items, Address address) async {
final response = await _dio.post('/orders', data: {
'items': items.map((i) => {'product_id': i.product.id, 'quantity': i.quantity}).toList(),
'shipping_address': address.toJson(),
});
return Order.fromJson(response.data);
}
Future<String> uploadProductImage(String filePath) async {
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(filePath),
});
final response = await _dio.post('/upload', data: formData);
return response.data['url'];
}
}
// Dio singleton setup
final dio = Dio(BaseOptions(
baseUrl: 'https://api.shopapp.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
))
..interceptors.add(AuthInterceptor())
..interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
final productRepo = ProductRepository(dio);
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed — use `flutter run` on your local machine for hands-on comparison. Actual UI/state may vary slightly by platform.
❓ FAQ
dart run build_runner watch --delete-conflicting-outputs to continuously watch file changes and auto-generate.HttpClientAdapter to skip verification; in production, certificates must be verified.📖 Summary
- The http package is suitable for simple scenarios; Dio is for production projects needing interceptors, cancellation, and retry
- Interceptors uniformly handle token attachment and 401 auto-refresh
- CancelToken cancels stale requests (e.g., search box debounce)
- json_serializable code generation is type-safe and reduces manual errors
- ApiException uniformly wraps errors; the UI layer only needs to catch one exception type
📝 Exercises
- Basic (⭐): Use the http package to make a GET request, fetch a product list, and print the JSON.
- Intermediate (⭐⭐): Wrap a Repository with Dio, add an Auth interceptor to auto-attach Bearer Token, and auto-refresh on 401.
- Challenge (⭐⭐⭐): Implement a complete networking layer: Dio + Auth interceptor + Log interceptor + CancelToken search debounce + ApiException unified error handling + json_serializable models.