在Flutter开发中,网络请求是每个应用都绕不开的核心功能。传统的HTTP客户端使用方式往往需要编写大量重复代码,而Chopper的出现彻底改变了这一局面。Chopper是一个基于Dart的HTTP客户端生成器,它借鉴了Android平台上Retrofit的设计理念,通过注解和代码生成技术,让开发者能够以声明式的方式定义API接口。
Chopper之所以能在众多Flutter网络库中脱颖而出,主要得益于以下几个关键特性:
在Flutter生态中,除了Chopper外,还有几个常用的HTTP客户端库:
Chopper特别适合中大型项目,尤其是那些API接口数量多、变更频繁的应用。通过代码生成,它能显著提高开发效率,减少因手动编写请求代码导致的错误。
首先需要在项目的pubspec.yaml文件中添加Chopper及其相关依赖:
yaml复制dependencies:
chopper: ^8.4.0
json_annotation: ^4.9.0
http: ^1.5.0
dev_dependencies:
build_runner: ^2.4.9
chopper_generator: ^8.4.0
json_serializable: ^6.8.0
这里有几个关键点需要注意:
添加完依赖后,在项目根目录运行以下命令安装:
bash复制flutter pub get
这个命令会下载所有声明的依赖包。如果遇到版本冲突,可以尝试运行flutter pub upgrade来升级依赖。
对于更复杂的项目,可以创建build.yaml文件来配置代码生成选项:
yaml复制targets:
$default:
builders:
chopper_generator|chopper:
enabled: true
json_serializable|json_serializable:
enabled: true
这个配置文件可以控制代码生成的行为,比如启用或禁用某些生成器。对于大多数项目来说,使用默认配置就足够了。
在Flutter中使用Chopper时,通常需要先定义数据模型。以下是一个典型的资源模型定义:
dart复制import 'package:json_annotation/json_annotation.dart';
part 'resource.g.dart';
@JsonSerializable()
class Resource {
final String id;
final String name;
final DateTime createdAt;
Resource({
required this.id,
required this.name,
required this.createdAt,
});
factory Resource.fromJson(Map<String, dynamic> json) =>
_$ResourceFromJson(json);
Map<String, dynamic> toJson() => _$ResourceToJson(this);
}
关键点说明:
@JsonSerializable()注解告诉json_serializable需要为这个类生成序列化代码part 'resource.g.dart'引入即将生成的代码文件接下来定义API服务接口,这是Chopper的核心部分:
dart复制import 'package:chopper/chopper.dart';
import 'resource.dart';
part 'api_service.chopper.dart';
@ChopperApi(baseUrl: '/api/v1/resources')
abstract class ResourceService extends ChopperService {
static ResourceService create([ChopperClient? client]) =>
_$ResourceService(client);
@Get(path: '/{id}')
Future<Response<Resource>> getResource(@Path('id') String id);
@Get()
Future<Response<List<Resource>>> listResources({
@Query('page') int page = 1,
@Query('per_page') int perPage = 20,
});
@Post()
Future<Response<Resource>> createResource(@Body() Resource resource);
@Put(path: '/{id}')
Future<Response<Resource>> updateResource(
@Path('id') String id,
@Body() Resource resource,
);
@Delete(path: '/{id}')
Future<Response<void>> deleteResource(@Path('id') String id);
}
这个接口定义了几个关键元素:
@ChopperApi注解指定了基础URL路径定义好接口后,需要运行代码生成器来生成实现代码:
bash复制flutter pub run build_runner build
或者使用watch模式,在文件更改时自动重新生成:
bash复制flutter pub run build_runner watch
这个命令会生成两个文件:
提示:如果遇到生成错误,可以尝试先清理旧的生成文件:
bash复制flutter pub run build_runner clean flutter pub run build_runner build --delete-conflicting-outputs
创建一个ChopperClient实例是使用Chopper的第一步:
dart复制final chopperClient = ChopperClient(
baseUrl: Uri.parse('https://api.example.com'),
services: [
ResourceService.create(),
],
converter: JsonConverter(),
interceptors: [
HttpLoggingInterceptor(),
],
);
这个配置包含了几个关键部分:
虽然Chopper提供了基础的JsonConverter,但对于复杂项目,通常需要自定义转换器:
dart复制class CustomJsonConverter extends JsonConverter {
final Map<Type, Function> typeToJsonFactory;
const CustomJsonConverter(this.typeToJsonFactory);
@override
Future<Response<BodyType>> convertResponse<BodyType, InnerType>(
Response response,
) async {
final jsonRes = await super.convertResponse(response);
if (jsonRes.body is String) {
// 空响应处理
if (jsonRes.body.isEmpty) {
return jsonRes.copyWith<BodyType>(body: null);
}
// 尝试解析JSON字符串
try {
final body = json.decode(jsonRes.body);
return jsonRes.copyWith<BodyType>(
body: _decode<InnerType>(body),
);
} catch (e) {
return jsonRes.copyWith<BodyType>(body: jsonRes.body);
}
}
return jsonRes.copyWith<BodyType>(
body: _decode<InnerType>(jsonRes.body),
);
}
dynamic _decode<T>(dynamic entity) {
if (entity is List) {
return entity.map<T>((item) => _decode<T>(item)).toList();
}
if (entity is Map) {
final factory = typeToJsonFactory[T];
if (factory != null) {
return factory(entity);
}
}
return entity;
}
}
使用自定义转换器:
dart复制final converter = CustomJsonConverter({
Resource: Resource.fromJson,
User: User.fromJson,
});
final chopperClient = ChopperClient(
converter: converter,
// 其他配置...
);
拦截器是Chopper的强大功能之一,可以用于各种横切关注点:
dart复制class AuthInterceptor implements RequestInterceptor {
final String token;
AuthInterceptor(this.token);
@override
Future<Request> onRequest(Request request) async {
final newRequest = applyHeaders(request, {
'Authorization': 'Bearer $token',
});
return newRequest;
}
}
dart复制class CustomLoggingInterceptor implements RequestInterceptor, ResponseInterceptor {
@override
Future<Request> onRequest(Request request) async {
print('--> ${request.method} ${request.url}');
print('Headers: ${request.headers}');
if (request.body != null) {
print('Body: ${request.body}');
}
print('--> END ${request.method}');
return request;
}
@override
Future<Response> onResponse(Response response) async {
print('<-- ${response.statusCode} ${response.request?.url}');
print('Headers: ${response.headers}');
print('Body: ${response.body}');
print('<-- END HTTP');
return response;
}
}
dart复制class ErrorInterceptor implements ResponseInterceptor {
@override
Future<Response> onResponse(Response response) async {
if (response.statusCode >= 400) {
throw ApiException(
response.statusCode,
response.body.toString(),
);
}
return response;
}
}
将这些拦截器添加到客户端:
dart复制final chopperClient = ChopperClient(
interceptors: [
AuthInterceptor('your_token'),
CustomLoggingInterceptor(),
ErrorInterceptor(),
(response) async {
// 简单的匿名拦截器
print('Request took: ${response.timeout}');
return response;
},
],
// 其他配置...
);
配置好客户端后,可以这样获取服务实例:
dart复制final resourceService = chopperClient.getService<ResourceService>();
dart复制// 获取单个资源
final response = await resourceService.getResource('123');
if (response.isSuccessful) {
final resource = response.body;
print('Got resource: ${resource?.name}');
} else {
print('Error: ${response.statusCode}');
}
// 获取资源列表(带分页参数)
final listResponse = await resourceService.listResources(
page: 2,
perPage: 10,
);
dart复制final newResource = Resource(
id: 'new_123',
name: 'New Resource',
createdAt: DateTime.now(),
);
final createResponse = await resourceService.createResource(newResource);
if (createResponse.isSuccessful) {
print('Created: ${createResponse.body?.id}');
}
dart复制final updatedResource = Resource(
id: '123',
name: 'Updated Name',
createdAt: DateTime.now(),
);
final updateResponse = await resourceService.updateResource(
'123',
updatedResource,
);
dart复制final deleteResponse = await resourceService.deleteResource('123');
if (deleteResponse.isSuccessful) {
print('Resource deleted');
}
Chopper的Response对象提供了丰富的信息:
dart复制final response = await resourceService.getResource('123');
// 状态信息
print('Status: ${response.statusCode}');
print('Message: ${response.base.request?.url}');
// 响应头
print('Headers:');
response.headers.forEach((key, value) {
print('$key: $value');
});
// 响应体
if (response.isSuccessful) {
final resource = response.body;
print('Resource: ${resource?.toJson()}');
} else {
print('Error: ${response.error}');
}
良好的错误处理是网络请求的关键:
dart复制try {
final response = await resourceService.getResource('invalid_id');
if (!response.isSuccessful) {
// 处理HTTP错误
throw ApiError.fromResponse(response);
}
// 处理成功响应
print(response.body);
} on ApiError catch (e) {
// 自定义API错误
print('API Error: ${e.message}');
} on SocketException catch (_) {
// 网络连接错误
print('Network error');
} catch (e) {
// 其他未知错误
print('Unknown error: $e');
}
Chopper支持文件上传等复杂请求:
dart复制@Post(path: '/upload')
@multipart
Future<Response<UploadResult>> uploadFile(
@PartFile('file') List<int> bytes,
@Part('description') String description,
);
使用示例:
dart复制final imageFile = File('path/to/image.jpg');
final bytes = await imageFile.readAsBytes();
final uploadResponse = await resourceService.uploadFile(
bytes,
'This is an image description',
);
可以通过多种方式添加请求头:
方法1:在方法注解中直接指定
dart复制@Get(headers: {
'Cache-Control': 'no-cache',
'Accept': 'application/json',
})
Future<Response<List<Resource>>> listResources();
方法2:使用@Header注解
dart复制@Get()
Future<Response<List<Resource>>> listResources(
@Header('Authorization') String token,
);
方法3:通过拦截器统一添加
dart复制class ContentTypeInterceptor implements RequestInterceptor {
@override
Future<Request> onRequest(Request request) async {
return applyHeaders(request, {
'Content-Type': 'application/json',
});
}
}
可以通过自定义http.Client来配置超时:
dart复制final httpClient = HttpClient()
..connectionTimeout = const Duration(seconds: 10);
final chopperClient = ChopperClient(
client: httpClient,
// 其他配置...
);
Chopper可以轻松与测试框架集成:
dart复制test('getResource returns expected data', () async {
// 创建Mock客户端
final mockClient = MockChopperClient(
(request) async {
if (request.url.path == '/api/v1/resources/123') {
return Response(
200,
{'id': '123', 'name': 'Test Resource'},
request,
);
}
return Response(404, {}, request);
},
);
// 创建服务实例
final service = ResourceService.create(mockClient);
// 测试API调用
final response = await service.getResource('123');
expect(response.isSuccessful, true);
expect(response.body?.id, '123');
expect(response.body?.name, 'Test Resource');
});
对于大型项目,推荐的组织结构:
code复制lib/
├── data/
│ ├── models/ # 数据模型
│ ├── api/ # API服务定义
│ ├── repositories/ # 数据仓库
│ └── sources/ # 数据源
├── core/
│ ├── network/ # 网络相关
│ │ ├── client.dart # Chopper客户端配置
│ │ ├── interceptors/ # 拦截器
│ │ └── converters/ # 转换器
│ └── exceptions/ # 异常处理
└── features/ # 功能模块
└── resources/ # 示例资源模块
├── bloc/ # 业务逻辑
└── ui/ # 界面组件
问题现象:运行build_runner时报错
可能原因:
解决方案:
bash复制flutter pub run build_runner clean
bash复制flutter pub run build_runner build --delete-conflicting-outputs
问题现象:类型转换异常
解决方案:
dart复制@JsonSerializable()
class Resource {
final String? id; // 可空字段
final String name;
// ...
}
排查步骤:
优化建议:
dart复制final httpClient = HttpClient()
..connectionTimeout = const Duration(seconds: 30);
dart复制class RetryInterceptor implements RequestInterceptor {
final int maxRetries;
RetryInterceptor({this.maxRetries = 3});
@override
Future<Request> onRequest(Request request) async {
int attempt = 0;
Response? response;
while (attempt < maxRetries) {
try {
response = await request.client.send(request);
if (response.statusCode < 500) {
return request;
}
} catch (e) {
// 忽略错误,继续重试
}
attempt++;
await Future.delayed(const Duration(seconds: 1));
}
return request;
}
}
dart复制final apiClientProvider = Provider<ChopperClient>((ref) {
return ChopperClient(
baseUrl: Uri.parse('https://api.example.com'),
interceptors: [
AuthInterceptor(ref.read(authTokenProvider)),
],
);
});
final resourceServiceProvider = Provider<ResourceService>((ref) {
return ref.read(apiClientProvider).getService<ResourceService>();
});
dart复制class ResourceBloc extends Bloc<ResourceEvent, ResourceState> {
final ResourceService _service;
ResourceBloc(this._service) : super(ResourceInitial()) {
on<LoadResource>((event, emit) async {
emit(ResourceLoading());
try {
final response = await _service.getResource(event.id);
if (response.isSuccessful) {
emit(ResourceLoaded(response.body!));
} else {
emit(ResourceError('Failed to load resource'));
}
} catch (e) {
emit(ResourceError(e.toString()));
}
});
}
}
Chopper允许创建自定义注解来扩展功能:
dart复制class CacheControl extends RequestInterceptor {
final Duration duration;
const CacheControl(this.duration);
@override
Future<Request> onRequest(Request request) async {
return applyHeaders(request, {
'Cache-Control': 'max-age=${duration.inSeconds}',
});
}
}
// 使用自定义注解
@Get()
@CacheControl(Duration(minutes: 5))
Future<Response<List<Resource>>> listResources();
在真实项目中,认证管理通常需要特别处理:
dart复制class AuthManager {
final ChopperClient client;
final SharedPreferences prefs;
AuthManager(this.client, this.prefs);
Future<void> login(String email, String password) async {
final authService = client.getService<AuthService>();
final response = await authService.login(email, password);
if (response.isSuccessful) {
final token = response.body!.token;
await prefs.setString('auth_token', token);
// 更新所有请求的认证头
client.interceptors.removeWhere((i) => i is AuthInterceptor);
client.interceptors.add(AuthInterceptor(token));
}
}
Future<void> logout() async {
await prefs.remove('auth_token');
client.interceptors.removeWhere((i) => i is AuthInterceptor);
}
}
实现文件下载进度反馈:
dart复制@Get()
@Streaming()
Future<Response<ByteData>> downloadFile(
@Path('id') String fileId,
@SendProgress() ProgressCallback? onSendProgress,
);
// 使用示例
final response = await resourceService.downloadFile(
'file123',
(int sent, int total) {
final progress = sent / total * 100;
print('Download progress: $progress%');
},
);
处理API版本升级的优雅方式:
dart复制abstract class ApiVersions {
static const v1 = 'v1';
static const v2 = 'v2';
}
@ChopperApi(baseUrl: '/${ApiVersions.v1}/resources')
abstract class ResourceServiceV1 extends ChopperService {
// v1接口...
}
@ChopperApi(baseUrl: '/${ApiVersions.v2}/resources')
abstract class ResourceServiceV2 extends ChopperService {
// v2接口...
}
// 客户端配置
final chopperClient = ChopperClient(
services: [
ResourceServiceV1.create(),
ResourceServiceV2.create(),
],
// ...
);
实现请求取消功能:
dart复制final cancelToken = CancelToken();
// 发起可取消的请求
final response = await resourceService.getResource(
'123',
cancelToken: cancelToken,
);
// 需要取消时
cancelToken.cancel('User cancelled');
// 在服务接口中添加CancelToken参数
@Get(path: '/{id}')
Future<Response<Resource>> getResource(
@Path('id') String id, {
CancelToken? cancelToken,
});
Chopper作为Flutter生态中强大的HTTP客户端生成器,通过其声明式的API定义方式和丰富的功能集,能够显著提升开发效率和代码质量。在实际项目中,合理使用Chopper可以:
对于想要进一步深入Chopper的开发者,可以考虑以下几个方向:
Chopper的活跃社区和良好扩展性使其成为Flutter网络请求的长期解决方案。随着Flutter生态的不断发展,Chopper也在持续进化,值得开发者投入时间学习和掌握。