Flutter+Dio实战:手把手教你封装一个健壮的GitCode API客户端(附完整避坑指南)

张开发
2026/4/12 19:09:30 15 分钟阅读

分享文章

Flutter+Dio实战:手把手教你封装一个健壮的GitCode API客户端(附完整避坑指南)
FlutterDio实战构建高可用GitCode API客户端的工程化实践在移动应用开发中网络请求如同血管般贯穿整个应用的生命周期。当我们需要与GitCode这类代码托管平台的API交互时一个设计良好的API客户端能显著提升开发效率和代码质量。本文将带你从工程化角度构建一个具备生产级质量的Flutter网络请求模块。1. 项目架构设计与基础配置优秀的API客户端始于合理的项目结构规划。我们采用分层设计思想将网络模块独立为可复用的基础设施层。推荐的项目目录结构如下lib/ ├── api/ │ ├── gitcode_client.dart # 主客户端类 │ ├── models/ # 数据模型 │ │ ├── user.dart │ │ ├── repository.dart │ ├── interceptors/ # 拦截器 │ │ ├── auth_interceptor.dart │ │ ├── log_interceptor.dart ├── config/ │ ├── api_config.dart # API配置 ├── utils/ │ ├── dio_utils.dart # Dio工具类在pubspec.yaml中添加必要的依赖dependencies: dio: ^5.4.0 logger: ^2.0.0 json_annotation: ^4.8.1 retrofit: ^4.0.1 dev_dependencies: build_runner: ^2.4.7 json_serializable: ^6.7.1 retrofit_generator: ^4.0.1安全配置是API客户端的重要环节。我们使用api_config.dart管理敏感信息abstract class ApiConfig { static const String baseUrl https://api.gitcode.com/api/v5; static const String token YOUR_ACCESS_TOKEN; // 实际项目中应从安全存储读取 }2. Dio核心配置与拦截器链Dio的强大之处在于其高度可定制的拦截器系统。我们构建一个完整的拦截器链来处理各种网络场景。基础配置示例class DioUtils { static Dio createDio() { final dio Dio(BaseOptions( baseUrl: ApiConfig.baseUrl, connectTimeout: const Duration(seconds: 8), receiveTimeout: const Duration(seconds: 10), contentType: Headers.jsonContentType, responseType: ResponseType.json, )); // 添加拦截器 dio.interceptors.addAll([ AuthInterceptor(), LoggingInterceptor(), ErrorInterceptor(), RetryInterceptor(), ]); return dio; } }认证拦截器实现class AuthInterceptor extends Interceptor { override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { // 动态获取token避免硬编码 final token SecureStorage.getToken(); if (token ! null) { options.headers[Authorization] Bearer $token; } super.onRequest(options, handler); } }日志拦截器增强版class LoggingInterceptor extends Interceptor { final Logger _logger Logger( printer: PrettyPrinter( methodCount: 0, errorMethodCount: 5, colors: true, ), ); override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { _logger.i(Request: ${options.method} ${options.uri}); super.onRequest(options, handler); } override void onResponse(Response response, ResponseInterceptorHandler handler) { _logger.i( Response: ${response.statusCode} Data: ${_formatJson(response.data)} ); super.onResponse(response, handler); } String _formatJson(dynamic data) { try { return const JsonEncoder.withIndent( ).convert(data); } catch (_) { return data.toString(); } } }3. 异常处理与重试机制健壮的错误处理是API客户端的核心能力。我们设计一个分层的错误处理体系。自定义异常类abstract class ApiException implements Exception { final String message; final int? statusCode; final DioException? dioError; const ApiException(this.message, [this.statusCode, this.dioError]); override String toString() ApiException: $message; } class NetworkException extends ApiException { const NetworkException(String message, [int? statusCode, DioException? error]) : super(message, statusCode, error); } class AuthException extends ApiException { const AuthException(String message) : super(message, 401); } class NotFoundException extends ApiException { const NotFoundException(String message) : super(message, 404); }错误拦截器实现class ErrorInterceptor extends Interceptor { override void onError(DioException err, ErrorInterceptorHandler handler) { final exception _transformError(err); if (exception is NetworkException _shouldRetry(err)) { handler.resolve(Response( requestOptions: err.requestOptions, statusCode: 503, statusMessage: Retry, )); } else { handler.reject(err); } } ApiException _transformError(DioException error) { switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.receiveTimeout: case DioExceptionType.sendTimeout: return NetworkException(请求超时, 408, error); case DioExceptionType.badResponse: return _handleResponseError(error); case DioExceptionType.cancel: return NetworkException(请求取消, -1, error); default: return NetworkException(网络连接异常, null, error); } } ApiException _handleResponseError(DioException error) { final statusCode error.response?.statusCode; final data error.response?.data; switch (statusCode) { case 400: return NetworkException(请求参数错误, statusCode, error); case 401: return AuthException(认证失败); case 403: return NetworkException(权限不足, statusCode, error); case 404: return NotFoundException(资源不存在); case 500: case 502: case 503: return NetworkException(服务不可用, statusCode, error); default: return NetworkException(服务器错误, statusCode, error); } } bool _shouldRetry(DioException err) { return err.type DioExceptionType.connectionTimeout || err.response?.statusCode 503; } }智能重试机制class RetryInterceptor extends Interceptor { final int maxRetries; final Duration retryInterval; RetryInterceptor({ this.maxRetries 3, this.retryInterval const Duration(seconds: 1), }); override Future onError(DioException err, ErrorInterceptorHandler handler) async { final shouldRetry err.response?.statusCode 503 || err.type DioExceptionType.connectionTimeout; if (!shouldRetry || _getRetryCount(err) maxRetries) { return super.onError(err, handler); } await Future.delayed(retryInterval); err.requestOptions.extra[retry_count] (_getRetryCount(err) ?? 0) 1; try { final response await Dio(err.requestOptions).fetch(err.requestOptions); handler.resolve(response); } on DioException catch (e) { super.onError(e, handler); } } int? _getRetryCount(DioException err) { return err.requestOptions.extra[retry_count] as int?; } }4. 模型设计与JSON序列化使用json_serializable实现类型安全的模型转换用户模型示例JsonSerializable() class GitCodeUser { final String login; JsonKey(name: avatar_url) final String avatarUrl; final String? name; final String? bio; JsonKey(name: html_url) final String? htmlUrl; JsonKey(name: public_repos) final int? publicRepos; final int? followers; final int? following; JsonKey(name: created_at) final DateTime? createdAt; GitCodeUser({ required this.login, required this.avatarUrl, this.name, this.bio, this.htmlUrl, this.publicRepos, this.followers, this.following, this.createdAt, }); factory GitCodeUser.fromJson(MapString, dynamic json) _$GitCodeUserFromJson(json); MapString, dynamic toJson() _$GitCodeUserToJson(this); }仓库模型增强版JsonSerializable() class GitCodeRepository { JsonKey(name: full_name) final String fullName; JsonKey(name: html_url) final String htmlUrl; final String? description; final String? language; JsonKey(name: updated_at) final DateTime? updatedAt; JsonKey(name: stargazers_count) final int stars; JsonKey(name: forks_count) final int forks; JsonKey(name: watchers_count) final int watchers; JsonKey(name: owner) final RepositoryOwner owner; final bool isPrivate; GitCodeRepository({ required this.fullName, required this.htmlUrl, this.description, this.language, this.updatedAt, required this.stars, required this.forks, required this.watchers, required this.owner, required this.isPrivate, }); factory GitCodeRepository.fromJson(MapString, dynamic json) _$GitCodeRepositoryFromJson(json); MapString, dynamic toJson() _$GitCodeRepositoryToJson(this); } JsonSerializable() class RepositoryOwner { final String login; JsonKey(name: avatar_url) final String avatarUrl; RepositoryOwner({ required this.login, required this.avatarUrl, }); factory RepositoryOwner.fromJson(MapString, dynamic json) _$RepositoryOwnerFromJson(json); MapString, dynamic toJson() _$RepositoryOwnerToJson(this); }5. API客户端实现与最佳实践使用Retrofit简化API定义RestApi(baseUrl: ApiConfig.baseUrl) abstract class GitCodeApiService { factory GitCodeApiService(Dio dio, {String baseUrl}) _GitCodeApiService; GET(/users/{username}) FutureGitCodeUser getUser( Path(username) String username, Header(Authorization) String token, ); GET(/search/users) FutureUserSearchResult searchUsers( Query(q) String query, Query(page) int page, Query(per_page) int perPage, Header(Authorization) String token, ); GET(/search/repositories) FutureRepositorySearchResult searchRepositories( Query(q) String query, Query(sort) String? sort, Query(order) String? order, Query(page) int page, Query(per_page) int perPage, Header(Authorization) String token, ); }客户端包装类实现class GitCodeClient { final GitCodeApiService _apiService; final Dio _dio; GitCodeClient() : _dio DioUtils.createDio(), _apiService GitCodeApiService(DioUtils.createDio()); FutureGitCodeUser getUser(String username) async { try { final token await SecureStorage.getToken(); return await _apiService.getUser(username, Bearer $token); } on DioException catch (e) { throw ApiException.fromDioError(e); } } FutureUserSearchResult searchUsers({ required String query, int page 1, int perPage 10, }) async { try { final token await SecureStorage.getToken(); return await _apiService.searchUsers( query, page, perPage, Bearer $token, ); } on DioException catch (e) { throw ApiException.fromDioError(e); } } FutureRepositorySearchResult searchRepositories({ required String query, String? sort, String? order, int page 1, int perPage 10, }) async { try { final token await SecureStorage.getToken(); return await _apiService.searchRepositories( query, sort, order, page, perPage, Bearer $token, ); } on DioException catch (e) { throw ApiException.fromDioError(e); } } }6. 性能优化与高级特性请求缓存策略实现class CacheInterceptor extends Interceptor { final CacheStore _cache; CacheInterceptor(this._cache); override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { if (options.extra[no_cache] true) { return handler.next(options); } final cacheKey _getCacheKey(options); final cached _cache.get(cacheKey); if (cached ! null) { handler.resolve(Response( requestOptions: options, data: cached, statusCode: 304, statusMessage: CACHE, )); } else { handler.next(options); } } override void onResponse(Response response, ResponseInterceptorHandler handler) { if (response.requestOptions.extra[no_cache] true || response.statusCode ! 200) { return handler.next(response); } final cacheKey _getCacheKey(response.requestOptions); _cache.set(cacheKey, response.data); handler.next(response); } String _getCacheKey(RequestOptions options) { return ${options.method}:${options.path}:${options.queryParameters}; } }请求节流与防抖class ThrottleInterceptor extends Interceptor { final MapString, DateTime _lastRequests {}; final Duration throttleDuration; ThrottleInterceptor({this.throttleDuration const Duration(milliseconds: 500)}); override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final key ${options.method}:${options.path}; final now DateTime.now(); final lastRequestTime _lastRequests[key]; if (lastRequestTime ! null now.difference(lastRequestTime) throttleDuration) { return handler.reject(DioException( requestOptions: options, error: 请求过于频繁, type: DioExceptionType.cancel, )); } _lastRequests[key] now; handler.next(options); } }7. 测试策略与Mock实现单元测试配置void main() { late GitCodeApiService apiService; late Dio mockDio; setUp(() { mockDio MockDio(); apiService GitCodeApiService(mockDio); }); test(getUser returns User when response is 200, () async { when(mockDio.get(any, options: anyNamed(options))) .thenAnswer((_) async Response( requestOptions: RequestOptions(path: ), statusCode: 200, data: { login: testuser, avatar_url: https://example.com/avatar, }, )); final user await apiService.getUser(testuser, token); expect(user.login, testuser); }); test(getUser throws when response is 404, () async { when(mockDio.get(any, options: anyNamed(options))) .thenAnswer((_) async Response( requestOptions: RequestOptions(path: ), statusCode: 404, )); expect( () apiService.getUser(testuser, token), throwsA(isADioException()), ); }); }集成测试示例void main() { late GitCodeClient client; late MockSecureStorage mockStorage; setUp(() { mockStorage MockSecureStorage(); SecureStorage.instance mockStorage; client GitCodeClient(); }); test(searchUsers returns results for valid query, () async { when(mockStorage.getToken()).thenAnswer((_) async test_token); final result await client.searchUsers(query: flutter); expect(result.items, isNotEmpty); expect(result.items.first, isAGitCodeUser()); }); test(searchUsers throws AuthException for invalid token, () async { when(mockStorage.getToken()).thenAnswer((_) async null); expect( () client.searchUsers(query: flutter), throwsA(isAAuthException()), ); }); }

更多文章