Flutter网络请求与序列化—Dio、json_serializable与错误处理|新宇宙博客
Back to list网络请求与序列化
Site Owner
Published on 2026-05-22
系统讲解Flutter网络层架构、Dio配置与拦截器、JSON序列化代码生成、统一错误处理及离线缓存方案
网络请求与序列化 (HTTP & Serialization)
模块:七 - 网络与数据持久化
预计阅读时间:30 分钟
前言
在任何真实的 Flutter 应用中,网络层的质量直接决定了用户体验的上限。一个设计良好的网络层需要解决:
鉴权 :Token 自动刷新,并发请求下的锁机制
错误处理 :统一的错误格式,友好的用户提示
序列化 :类型安全的模型层,避免运行时崩溃
可测试性 :能轻松 mock 的依赖设计
本文从 http 包到 dio + retrofit + freezed 的完整技术栈,构建一套生产就绪的网络层。
一、http vs dio:设计哲学对比
1.1 官方 http 包
Flutter 官方 http 包追求极简:
import 'package:http/http.dart' as http;
// GET 请求
final response = await http.get(
Uri.parse('https://api.example.com/users'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
// 处理数据...
}
局限性 :
无拦截器机制(每次请求都要手动加 header)
无请求取消
无上传进度回调
无响应缓存
1.2 dio:生产级网络库
dependencies:
dio: ^5.4.0
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
// 1. 取消请求
final cancelToken = CancelToken();
dio.get('/users', cancelToken: cancelToken);
cancelToken.cancel('用户离开页面');
// 2. 上传进度
dio.post(
'/upload',
data: FormData.fromMap({'file': await MultipartFile.fromFile('./photo.jpg')}),
onSendProgress: (sent, total) {
print('进度:${(sent / total * 100).toStringAsFixed(0)}%');
},
);
// 3. 下载文件
dio.download(
'https://example.com/file.pdf',
'/path/to/save/file.pdf',
onReceiveProgress: (received, total) {
if (total != -1) print('${(received / total * 100).toStringAsFixed(0)}%');
},
);
二、拦截器链:洋葱模型 Dio 的拦截器是最强大的特性之一,采用洋葱模型处理请求和响应:
Request → [Interceptor1.onRequest] → [Interceptor2.onRequest] → Server
Response → [Interceptor2.onResponse] → [Interceptor1.onResponse] → Code
Error → [Interceptor1.onError] → [Interceptor2.onError] → Code
2.1 日志拦截器 class LogInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
debugPrint('→ ${options.method} ${options.uri}');
debugPrint(' Headers: ${options.headers}');
if (options.data != null) debugPrint(' Body: ${options.data}');
handler.next(options); // 继续传递
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
debugPrint('← ${response.statusCode} ${response.requestOptions.uri}');
debugPrint(' Body: ${response.data}');
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
debugPrint('✗ ${err.type}: ${err.message}');
handler.next(err);
}
}
2.2 Auth 拦截器:自动添加 Token class AuthInterceptor extends Interceptor {
final TokenStorage tokenStorage;
AuthInterceptor(this.tokenStorage);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
final token = await tokenStorage.getAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
}
2.3 Token 刷新拦截器:处理并发的锁机制 这是网络层最复杂的部分。当 Token 过期时,多个并发请求同时收到 401,需要:
只发起一次 Token 刷新请求
让其他请求等待 刷新完成
刷新成功后,用新 Token 重试 所有等待的请求
class TokenRefreshInterceptor extends Interceptor {
final Dio dio;
final AuthRepository authRepository;
// 防止并发刷新的锁
bool _isRefreshing = false;
// 等待刷新的请求队列
final List<({RequestOptions options, ErrorInterceptorHandler handler})>
_pendingRequests = [];
TokenRefreshInterceptor({
required this.dio,
required this.authRepository,
});
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 只处理 401 错误
if (err.response?.statusCode != 401) {
handler.next(err);
return;
}
// 如果正在刷新,将此请求加入等待队列
if (_isRefreshing) {
_pendingRequests.add((options: err.requestOptions, handler: handler));
return;
}
_isRefreshing = true;
try {
// 刷新 Token
final newTokens = await authRepository.refreshToken();
await authRepository.saveTokens(newTokens);
// 重试当前请求
final retryResponse = await _retry(
err.requestOptions,
newTokens.accessToken,
);
handler.resolve(retryResponse);
// 重试所有等待的请求
for (final pending in _pendingRequests) {
try {
final response = await _retry(
pending.options,
newTokens.accessToken,
);
pending.handler.resolve(response);
} catch (e) {
pending.handler.reject(
DioException(requestOptions: pending.options, error: e),
);
}
}
} catch (e) {
// Token 刷新失败(Refresh Token 也过期了)
// 清除所有 Token,跳转到登录页
await authRepository.logout();
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: 'Session expired',
type: DioExceptionType.unknown,
),
);
// 通知所有等待的请求失败
for (final pending in _pendingRequests) {
pending.handler.reject(
DioException(
requestOptions: pending.options,
error: 'Session expired',
),
);
}
} finally {
_isRefreshing = false;
_pendingRequests.clear();
}
}
Future<Response<dynamic>> _retry(
RequestOptions options,
String newToken,
) {
return dio.request<dynamic>(
options.path,
data: options.data,
queryParameters: options.queryParameters,
options: Options(
method: options.method,
headers: {
...options.headers,
'Authorization': 'Bearer $newToken',
},
),
);
}
}
三、JSON 序列化三种方案对比
3.1 方案一:手写 fromJson/toJson class User {
final int id;
final String name;
final String email;
final DateTime createdAt;
const User({
required this.id,
required this.name,
required this.email,
required this.createdAt,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'created_at': createdAt.toIso8601String(),
};
}
}
适合场景 :小型项目,模型少于 10 个,对依赖敏感的库。
缺点 :容易在类型转换时出 bug,大量重复代码。
3.2 方案二:json_serializable 代码生成 dependencies:
json_annotation: ^4.8.0
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.7.0
import 'package:json_annotation/json_annotation.dart';
// 告诉 build_runner 为此类生成代码
part 'user.g.dart'; // 生成文件的名称
@JsonSerializable(
// explicitToJson: true 递归地调用嵌套对象的 toJson
explicitToJson: true,
// fieldRename: FieldRename.snake 自动将驼峰字段映射到下划线
fieldRename: FieldRename.snake,
)
class User {
final int id;
final String name;
final String email;
final DateTime createdAt;
// 自定义字段名映射(覆盖全局设置)
@JsonKey(name: 'avatar_url')
final String? avatarUrl;
// 忽略该字段(不序列化/反序列化)
@JsonKey(includeFromJson: false, includeToJson: false)
bool get isAdmin => id < 100;
const User({
required this.id,
required this.name,
required this.email,
required this.createdAt,
this.avatarUrl,
});
// 生成的工厂构造函数
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// 生成的 toJson 方法
Map<String, dynamic> toJson() => _$UserToJson(this);
}
flutter pub run build_runner build --delete-conflicting-outputs
flutter pub run build_runner watch --delete-conflicting-outputs
3.3 方案三:freezed + json_serializable(推荐) freezed 在 json_serializable 基础上增加了不可变性 和联合类型(Union Types) :
dependencies:
freezed_annotation: ^2.4.0
json_annotation: ^4.8.0
dev_dependencies:
build_runner: ^2.4.0
freezed: ^2.4.0
json_serializable: ^6.7.0
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
required String email,
@JsonKey(name: 'created_at') required DateTime createdAt,
String? avatarUrl,
@Default(false) bool isVip, // 默认值
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// freezed 自动生成:
// 1. copyWith:不可变更新
// 2. == 和 hashCode:基于值的相等性
// 3. toString:友好的打印格式
// 4. fromJson / toJson(结合 json_serializable)
// 使用示例:
void example() {
const user = User(
id: 1,
name: 'Alice',
email: 'alice@example.com',
createdAt: /* ... */,
);
// copyWith:不可变更新
final updatedUser = user.copyWith(name: 'Alice Smith');
// 值相等性
print(user == updatedUser); // false
}
freezed 的联合类型(API 状态建模的利器) @freezed
class ApiState<T> with _$ApiState<T> {
const factory ApiState.initial() = Initial;
const factory ApiState.loading() = Loading;
const factory ApiState.success(T data) = Success;
const factory ApiState.error(String message, {int? statusCode}) = ApiError;
}
// 使用:exhaustive pattern matching
Widget buildByState(ApiState<User> state) {
return switch (state) {
Initial() => const SizedBox(),
Loading() => const CircularProgressIndicator(),
Success(data: final user) => Text('Hello, ${user.name}'),
ApiError(message: final msg) => Text('Error: $msg'),
};
}
四、Retrofit:声明式 API 定义 retrofit 为 dio 提供类似 Java Retrofit 的声明式 API 定义方式:
dependencies:
retrofit: ^4.1.0
dio: ^5.4.0
dev_dependencies:
retrofit_generator: ^8.1.0
build_runner: ^2.4.0
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';
part 'user_api.g.dart';
@RestApi(baseUrl: 'https://api.example.com')
abstract class UserApi {
factory UserApi(Dio dio, {String baseUrl}) = _UserApi;
// GET /users?page=1&limit=20
@GET('/users')
Future<PaginatedResponse<User>> getUsers({
@Query('page') int page = 1,
@Query('limit') int limit = 20,
});
// GET /users/{id}
@GET('/users/{id}')
Future<User> getUserById(@Path('id') int id);
// POST /users
@POST('/users')
Future<User> createUser(@Body() CreateUserRequest request);
// PUT /users/{id}
@PUT('/users/{id}')
Future<User> updateUser(
@Path('id') int id,
@Body() UpdateUserRequest request,
);
// DELETE /users/{id}
@DELETE('/users/{id}')
Future<void> deleteUser(@Path('id') int id);
// 带文件上传
@MultiPart()
@POST('/users/{id}/avatar')
Future<String> uploadAvatar(
@Path('id') int id,
@Part() File file,
);
// 自定义响应类型
@GET('/users/export')
@DioResponseType(ResponseType.bytes)
Future<List<int>> exportUsers();
}
final dio = Dio()
..interceptors.addAll([
AuthInterceptor(tokenStorage),
TokenRefreshInterceptor(dio: dio, authRepository: authRepo),
LogInterceptor(),
]);
final userApi = UserApi(dio);
// 使用起来非常干净
final users = await userApi.getUsers(page: 1);
final user = await userApi.getUserById(42);
五、统一错误处理
5.1 定义领域错误类型 @freezed
sealed class AppException with _$AppException implements Exception {
// 网络连接错误
const factory AppException.networkError({
required String message,
}) = NetworkError;
// 服务器错误(4xx/5xx)
const factory AppException.serverError({
required int statusCode,
required String message,
String? errorCode,
}) = ServerError;
// 数据解析错误
const factory AppException.parseError({
required String message,
required Object cause,
}) = ParseError;
// 认证错误(token 刷新失败等)
const factory AppException.authError({
String message = '请重新登录',
}) = AuthError;
// 未知错误
const factory AppException.unknown({
required Object error,
}) = UnknownError;
}
5.2 DioException 到 AppException 的转换 extension DioExceptionConverter on DioException {
AppException toAppException() {
return switch (type) {
DioExceptionType.connectionTimeout ||
DioExceptionType.receiveTimeout ||
DioExceptionType.sendTimeout =>
AppException.networkError(message: '网络连接超时,请检查网络'),
DioExceptionType.connectionError =>
AppException.networkError(message: '无法连接到服务器,请检查网络'),
DioExceptionType.badResponse => _parseServerError(response!),
DioExceptionType.cancel =>
AppException.networkError(message: '请求已取消'),
_ => AppException.unknown(error: error ?? this),
};
}
AppException _parseServerError(Response response) {
final statusCode = response.statusCode ?? 0;
try {
final body = response.data as Map<String, dynamic>;
final message = body['message'] as String? ?? '服务器错误';
final errorCode = body['error_code'] as String?;
return AppException.serverError(
statusCode: statusCode,
message: message,
errorCode: errorCode,
);
} catch (_) {
return AppException.serverError(
statusCode: statusCode,
message: '服务器返回了不可识别的响应',
);
}
}
}
5.3 Repository 层的错误封装 class UserRepository {
final UserApi _api;
UserRepository(this._api);
Future<Result<User, AppException>> getUserById(int id) async {
try {
final user = await _api.getUserById(id);
return Result.success(user);
} on DioException catch (e) {
return Result.failure(e.toAppException());
} on TypeError catch (e) {
return Result.failure(AppException.parseError(
message: '数据格式错误',
cause: e,
));
}
}
}
// 简洁的 Result 类型(或者使用 either 包)
sealed class Result<S, F> {
const factory Result.success(S value) = Success;
const factory Result.failure(F error) = Failure;
}
class Success<S, F> implements Result<S, F> {
const Success(this.value);
final S value;
}
class Failure<S, F> implements Result<S, F> {
const Failure(this.error);
final F error;
}
六、完整的网络层架构
6.1 依赖关系图 UI Layer (Widget)
↓ 调用
ViewModel / Provider (状态管理)
↓ 调用
Repository (数据仓库,处理缓存策略)
↓ 调用
API Client (Retrofit 生成的接口)
↓ 通过
Dio Instance (含拦截器链)
↓ 发送
HTTP Server
6.2 完整的 DI 配置(使用 get_it) import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
void setupDependencies() {
// 1. 基础设施
getIt.registerSingleton<TokenStorage>(SecureTokenStorage());
getIt.registerSingleton<AuthRepository>(AuthRepositoryImpl(
tokenStorage: getIt<TokenStorage>(),
));
// 2. Dio 实例(带完整拦截器)
getIt.registerSingleton<Dio>(_createDio());
// 3. API 接口
getIt.registerSingleton<UserApi>(UserApi(getIt<Dio>()));
getIt.registerSingleton<ProductApi>(ProductApi(getIt<Dio>()));
// 4. Repository
getIt.registerSingleton<UserRepository>(
UserRepository(getIt<UserApi>()),
);
}
Dio _createDio() {
final dio = Dio(BaseOptions(
baseUrl: AppConfig.baseUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
));
dio.interceptors.addAll([
AuthInterceptor(getIt<TokenStorage>()),
TokenRefreshInterceptor(
dio: dio,
authRepository: getIt<AuthRepository>(),
),
if (kDebugMode) LogInterceptor(),
]);
return dio;
}
6.3 分页请求的通用处理 @freezed
class PaginatedResponse<T> with _$PaginatedResponse<T> {
const factory PaginatedResponse({
required List<T> items,
required int total,
required int page,
required int pageSize,
}) = _PaginatedResponse;
// 泛型 fromJson 需要传入子类型的 fromJson
factory PaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Object? json) fromJsonT,
) => _$PaginatedResponseFromJson(json, fromJsonT);
// 便捷计算属性
const PaginatedResponse._();
bool get hasNextPage => page * pageSize < total;
int get totalPages => (total / pageSize).ceil();
}
// Retrofit 中返回分页响应:
@GET('/products')
Future<PaginatedResponse<Product>> getProducts({
@Query('page') int page = 1,
@Query('limit') int limit = 20,
@Query('category') String? category,
});
七、网络请求的测试策略
7.1 Mock Dio 进行单元测试 import 'package:mocktail/mocktail.dart';
import 'package:dio/dio.dart';
class MockDio extends Mock implements Dio {}
class MockUserApi extends Mock implements UserApi {}
void main() {
late MockUserApi mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockUserApi();
repository = UserRepository(mockApi);
});
test('成功获取用户时应返回 Success', () async {
// Arrange
const user = User(id: 1, name: 'Alice', email: 'a@test.com', createdAt: /* ... */);
when(() => mockApi.getUserById(1)).thenAnswer((_) async => user);
// Act
final result = await repository.getUserById(1);
// Assert
expect(result, isA<Success<User, AppException>>());
expect((result as Success).value, equals(user));
});
test('网络超时时应返回 NetworkError', () async {
// Arrange
when(() => mockApi.getUserById(any())).thenThrow(
DioException(
requestOptions: RequestOptions(),
type: DioExceptionType.connectionTimeout,
),
);
// Act
final result = await repository.getUserById(1);
// Assert
expect(result, isA<Failure<User, AppException>>());
final error = (result as Failure).error;
expect(error, isA<NetworkError>());
});
}
7.2 使用 MockAdapter 模拟真实响应 import 'package:dio/dio.dart';
// 在测试/开发环境使用
final dio = Dio()
..httpClientAdapter = MockAdapter();
class MockAdapter implements HttpClientAdapter {
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async {
// 根据 URL 返回模拟数据
if (options.path.contains('/users')) {
return ResponseBody.fromString(
'{"id":1,"name":"Alice","email":"alice@test.com","created_at":"2024-01-01T00:00:00Z"}',
200,
headers: {Headers.contentTypeHeader: ['application/json']},
);
}
throw DioException(
requestOptions: options,
response: Response(requestOptions: options, statusCode: 404),
);
}
@override
void close({bool force = false}) {}
}
八、过关自测 问题一 :Token 刷新拦截器中为什么需要"锁"机制?不加锁会出现什么问题?
不加锁时,如果有 5 个并发请求同时收到 401,会触发 5 次 Token 刷新请求。其中 4 次会因为 Refresh Token 已被第一次刷新请求消耗而失败(服务端通常采用一次性 Refresh Token 策略)。锁机制确保只有第一个 401 触发刷新,其余等待,刷新完成后用新 Token 重试,避免了并发刷新的竞争条件。
问题二 :json_serializable 的代码生成原理是什么(build_runner + source_gen)?
build_runner 是 Dart 的通用代码生成脚手架,它扫描项目文件,找到带有特定注解的代码。source_gen 是 build_runner 的插件框架,json_serializable 基于它实现了 @JsonSerializable 注解的处理器。处理器分析带注解类的 AST(抽象语法树),生成对应的 .g.dart 文件,其中包含类型安全的 fromJson/toJson 实现。这些生成文件与源文件通过 part/part of 关联。
问题三 :freezed 相比纯 json_serializable 有哪些额外价值?
freezed 额外提供:① 深度不可变性(所有字段自动为 final);② 基于值的 == 和 hashCode(结合 List/Map 的深度比较);③ copyWith(类型安全的不可变更新);④ 联合类型(Union Types)用于建模 ADT(代数数据类型),如 sealed class ApiState { loading; success(data); error(msg) };⑤ 自动生成 toString。这些特性让数据模型更接近函数式编程风格,减少 null 相关的 bug。
总结 技术栈组件 职责 关键点 DioHTTP 客户端 拦截器链、取消、进度 Retrofit声明式 API 定义 代码生成,类型安全 freezed不可变数据模型 联合类型,copyWith json_serializableJSON 序列化 build_runner 代码生成 Token 刷新拦截器 鉴权自动化 并发锁、请求队列 AppException统一错误类型 将网络错误映射到领域错误
一个高质量的 Flutter 网络层,不是写几个 http.get 调用,而是一套类型安全、可测试、可维护的分层架构 。dio + retrofit + freezed 的组合是目前社区最成熟的选择,也是大多数中大型项目的事实标准。