Flutter: 网络请求与 REST API
没有网络的应用是座孤岛——API 请求是连接大陆的桥梁。
📋 前置知识:需要先掌握以下内容
- 第10课:列表与滚动
1. 你将学到
http包基础 GET/POST/PUT/DELETE 请求Dio深度应用:拦截器、CancelToken、FormData 上传、超时重试- JSON 序列化:
json_serializable+build_runner代码生成 vs 手动解析 - 错误处理策略:网络异常 / 401 鉴权 / 500 服务端错误统一拦截
- ShopApp Dio 封装 Repository 层,对接
/api/v1/products商品接口
2. 一个网络请求翻车的真实故事
(1) 痛点:网络错误让应用崩溃
Bob 的 ShopApp 上线第一天,用户在网络不稳定时直接白屏崩溃——没有 try-catch,没有超时,没有重试。更糟糕的是,Token 过期后所有请求返回 401,应用没有自动刷新 Token 的机制,用户被迫重新登录。一天内 2 thousand 个差评。
(2) Dio 拦截器的解法
Dio 的拦截器机制可以在请求发出前统一附加 Token,在响应返回后统一处理 401 自动刷新,网络超时自动重试。
DART
import 'package:dio/dio.dart';
// ⚙️ **安装依赖**:flutter pub add dio
// 假设 token 和 refreshToken 已在别处定义
// 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);
},
));
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(3) 收益:网络异常零崩溃
Bob 用 Dio 拦截器后,401 自动刷新、超时重试 3 次、统一错误提示。网络相关崩溃率从 5% 降到 0.01%。
3. HTTP 基础请求
(1) http 包 CRUD 操作
| 方法 | HTTP 动词 | 用途 |
|---|---|---|
http.get |
GET | 获取资源 |
http.post |
POST | 创建资源 |
http.put |
PUT | 更新资源 |
http.delete |
DELETE | 删除资源 |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:http 包基础请求
DART
import 'package:http/http.dart' as http;
import 'dart:convert';
// ⚙️ **安装依赖**:flutter pub add http
// 自定义类定义来源:
// - Product: 见本课第5节 json_serializable 模型
// - ApiException: 见本课第6节统一错误处理
// 假设 token 已在别处定义
// 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);
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
4. Dio 深度应用
(1) http vs Dio 对比
| 功能 | http | Dio |
|---|---|---|
| 基础 CRUD | ✅ | ✅ |
| 拦截器 | ❌ | ✅ |
| CancelToken | ❌ | ✅ |
| FormData 上传 | 手动 | ✅ |
| 超时重试 | 手动 | ✅ |
| 请求日志 | 手动 | ✅ (dio_logger) |
| 全局配置 | ❌ | ✅ BaseOptions |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:Dio 基础配置
DART
import 'package:dio/dio.dart';
// ⚙️ **安装依赖**:flutter pub add dio
// 自定义类定义来源:
// - Product: 见本课第5节 json_serializable 模型
// - Order/Cart: 见第14课 Phase 2 综合练习
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);
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:Auth 拦截器 + Token 刷新
DART
import 'package:dio/dio.dart';
// 自定义类定义来源:
// - SecureStorage: 见第13课 Flutter Secure Storage
// - AuthService: 见第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'];
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:CancelToken 取消请求
DART
import 'package:dio/dio.dart';
// ⚙️ **安装依赖**: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();
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
5. JSON 序列化
(1) 序列化方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 手动 fromJson/toJson | 无依赖、完全控制 | 重复代码、易出错 | 少量简单模型 |
| json_serializable | 类型安全、自动生成 | 需要 build_runner | 大量模型 |
| freezed | 不可变 + copyWith + union | 依赖更多 | 复杂领域模型 |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:json_serializable 模型
DART
import 'package:json_annotation/json_annotation.dart';
// ⚙️ **安装依赖**:flutter pub add json_annotation
// ⚙️ **开发依赖**: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
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
6. 统一错误处理
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:ApiException + 错误拦截器
DART
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
// ⚙️ **安装依赖**: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)),
);
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
7. 完整示例:ShopApp Dio Repository
DART
import 'package:dio/dio.dart';
// ⚙️ **安装依赖**:flutter pub add dio
// 自定义类定义来源:
// - Product: 见本课第5节 json_serializable 模型
// - CartItem/Address/Order: 见第14课 Phase 2 综合练习
// - AuthInterceptor: 见本课第4节 Auth 拦截器
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);
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
❓ 常见问题
Q Dio 和 http 包怎么选?
A 小型项目用 http 足够;需要拦截器、取消请求、重试、FormData 上传等功能时选 Dio。
Q json_serializable 每次 build_runner 很慢?
A 开发时用
dart run build_runner watch --delete-conflicting-outputs 持续监听文件变化自动生成。Q Token 刷新时多个请求同时 401 怎么办?
A 加锁机制:第一个 401 触发刷新,其他 401 请求排队等刷新完成。可用 Completer 实现。
Q FormData 上传大文件会 OOM 吗?
A MultipartFile.fromFile 是懒加载,不会一次性读入内存。大文件上传建议分片。
Q 开发环境怎么调试网络请求?
A 添加 LogInterceptor 打印请求/响应,或用 Charles/Proxyman 抓包,Dio 支持设置 proxy。
Q SSL 证书校验失败怎么办?
A 开发环境可用
HttpClientAdapter 跳过验证;生产环境必须验证证书。📖 小节
- http 包适合简单场景,Dio 适合需要拦截器、取消、重试的生产项目
- 拦截器统一处理 Token 附加和 401 自动刷新
- CancelToken 取消过期请求(如搜索框防抖)
- json_serializable 代码生成,类型安全且减少手写错误
- ApiException 统一封装错误,UI 层只需 catch 一种异常
📝 作业
- 基础题(难度⭐):用 http 包实现 GET 请求,获取商品列表并打印 JSON。
- 进阶题(难度⭐⭐):用 Dio 封装 Repository,添加 Auth 拦截器自动附加 Bearer Token,401 时自动刷新。
- 挑战题(难度⭐⭐⭐):实现完整的网络层:Dio + Auth 拦截器 + Log 拦截器 + CancelToken 搜索防抖 + ApiException 统一错误处理 + json_serializable 模型。