构建健壮的网络层架构

前言

网络请求是移动应用的核心基础设施。一个设计良好的网络层能够:

  • 统一管理:Token 刷新、请求日志、错误处理
  • 提升体验:自动重试、超时控制、离线缓存
  • 便于维护:集中配置、类型安全、易于测试

CleanMark AI 使用 Dio 作为网络库,通过拦截器实现统一的请求处理。这篇文章我会带你构建一个生产级的网络层。


一、网络层架构设计

1.1 分层结构

ApiClient(Dio 封装)
   ↓
Interceptors(拦截器链)
   ├── TokenInterceptor(Token 注入)
   ├── LogInterceptor(请求日志)
   ├── ErrorInterceptor(错误处理)
   └── RetryInterceptor(自动重试)
   ↓
DataSource(数据源)
   ↓
Repository(仓库)

1.2 核心组件

// lib/core/network/api_client.dart

import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../constants/api_constants.dart';
import 'interceptors/token_interceptor.dart';
import 'interceptors/log_interceptor.dart';
import 'interceptors/error_interceptor.dart';
import 'interceptors/retry_interceptor.dart';

/// API 客户端封装
class ApiClient {
  late final Dio _dio;

  ApiClient() {
    _dio = Dio(_baseOptions);
    _setupInterceptors();
  }

  /// 基础配置
  BaseOptions get _baseOptions => BaseOptions(
        baseUrl: ApiConstants.baseUrl,
        connectTimeout: const Duration(seconds: 30),
        receiveTimeout: const Duration(seconds: 30),
        sendTimeout: const Duration(seconds: 30),
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      );

  /// 配置拦截器
  void _setupInterceptors() {
    _dio.interceptors.addAll([
      TokenInterceptor(),      // Token 注入
      LogInterceptor(),        // 请求日志
      ErrorInterceptor(),      // 错误处理
      RetryInterceptor(_dio),  // 自动重试
    ]);
  }

  /// GET 请求
  Future<Response> get(
    String path, {
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {
    return await _dio.get(
      path,
      queryParameters: queryParameters,
      options: options,
    );
  }

  /// POST 请求
  Future<Response> post(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
    ProgressCallback? onSendProgress,
  }) async {
    return await _dio.post(
      path,
      data: data,
      queryParameters: queryParameters,
      options: options,
      onSendProgress: onSendProgress,
    );
  }

  /// PUT 请求
  Future<Response> put(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {
    return await _dio.put(
      path,
      data: data,
      queryParameters: queryParameters,
      options: options,
    );
  }

  /// DELETE 请求
  Future<Response> delete(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {
    return await _dio.delete(
      path,
      data: data,
      queryParameters: queryParameters,
      options: options,
    );
  }

  /// 下载文件
  Future<Response> download(
    String urlPath,
    String savePath, {
    ProgressCallback? onReceiveProgress,
    CancelToken? cancelToken,
  }) async {
    return await _dio.download(
      urlPath,
      savePath,
      onReceiveProgress: onReceiveProgress,
      cancelToken: cancelToken,
    );
  }
}

/// Provider 定义
final apiClientProvider = Provider<ApiClient>((ref) {
  return ApiClient();
});

本篇小结(第一部分)

这部分我们完成了:

  1. ✅ 网络层架构设计(分层结构)
  2. ✅ ApiClient 封装(Dio 配置)
  3. ✅ 基础请求方法(GET、POST、PUT、DELETE、下载)

下一部分我会继续讲解拦截器的实现。


二、拦截器实现

2.1 Token 拦截器

// lib/core/network/interceptors/token_interceptor.dart

import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// Token 拦截器,自动注入 JWT Token
class TokenInterceptor extends Interceptor {
  
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    // 从本地存储获取 Token
    final prefs = await SharedPreferences.getInstance();
    final token = prefs.getString('auth_token');

    if (token != null && token.isNotEmpty) {
      // 注入 Authorization 头
      options.headers['Authorization'] = 'Bearer $token';
    }

    handler.next(options);
  }

  
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    // Token 过期,尝试刷新
    if (err.response?.statusCode == 401) {
      try {
        final newToken = await _refreshToken();
        if (newToken != null) {
          // 保存新 Token
          final prefs = await SharedPreferences.getInstance();
          await prefs.setString('auth_token', newToken);

          // 重试原请求
          final options = err.requestOptions;
          options.headers['Authorization'] = 'Bearer $newToken';

          final response = await Dio().fetch(options);
          return handler.resolve(response);
        }
      } catch (e) {
        // Token 刷新失败,清除本地 Token,跳转登录页
        final prefs = await SharedPreferences.getInstance();
        await prefs.remove('auth_token');
      }
    }

    handler.next(err);
  }

  /// 刷新 Token
  Future<String?> _refreshToken() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final refreshToken = prefs.getString('refresh_token');

      if (refreshToken == null) return null;

      final response = await Dio().post(
        '/api/auth/refresh',
        data: {'refreshToken': refreshToken},
      );

      return response.data['token'] as String?;
    } catch (e) {
      return null;
    }
  }
}

2.2 日志拦截器

// lib/core/network/interceptors/log_interceptor.dart

import 'package:dio/dio.dart';
import 'package:logger/logger.dart';

/// 日志拦截器,记录请求和响应
class LogInterceptor extends Interceptor {
  final Logger _logger = Logger(
    printer: PrettyPrinter(
      methodCount: 0,
      errorMethodCount: 5,
      lineLength: 80,
      colors: true,
      printEmojis: true,
    ),
  );

  
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    _logger.d(
      '┌─────────────────────────────────────────────────────\n'
      '│ 请求: ${options.method} ${options.uri}\n'
      '│ Headers: ${options.headers}\n'
      '│ Data: ${options.data}\n'
      '└─────────────────────────────────────────────────────',
    );
    handler.next(options);
  }

  
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    _logger.i(
      '┌─────────────────────────────────────────────────────\n'
      '│ 响应: ${response.statusCode} ${response.requestOptions.uri}\n'
      '│ Data: ${response.data}\n'
      '└─────────────────────────────────────────────────────',
    );
    handler.next(response);
  }

  
  void onError(DioException err, ErrorInterceptorHandler handler) {
    _logger.e(
      '┌─────────────────────────────────────────────────────\n'
      '│ 错误: ${err.requestOptions.method} ${err.requestOptions.uri}\n'
      '│ 状态码: ${err.response?.statusCode}\n'
      '│ 错误信息: ${err.message}\n'
      '│ 响应数据: ${err.response?.data}\n'
      '└─────────────────────────────────────────────────────',
    );
    handler.next(err);
  }
}

2.3 错误拦截器

// lib/core/network/interceptors/error_interceptor.dart

import 'package:dio/dio.dart';

/// 错误拦截器,统一处理错误
class ErrorInterceptor extends Interceptor {
  
  void onError(DioException err, ErrorInterceptorHandler handler) {
    String errorMessage;

    switch (err.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        errorMessage = '网络连接超时,请检查网络设置';
        break;

      case DioExceptionType.badResponse:
        errorMessage = _handleStatusCode(err.response?.statusCode);
        break;

      case DioExceptionType.cancel:
        errorMessage = '请求已取消';
        break;

      case DioExceptionType.connectionError:
        errorMessage = '网络连接失败,请检查网络设置';
        break;

      case DioExceptionType.unknown:
        errorMessage = '未知错误,请重试';
        break;

      default:
        errorMessage = '网络请求失败: ${err.message}';
    }

    // 将错误信息附加到异常中
    handler.next(
      DioException(
        requestOptions: err.requestOptions,
        response: err.response,
        type: err.type,
        error: errorMessage,
        message: errorMessage,
      ),
    );
  }

  /// 处理 HTTP 状态码
  String _handleStatusCode(int? statusCode) {
    switch (statusCode) {
      case 400:
        return '请求参数错误';
      case 401:
        return '未授权,请重新登录';
      case 402:
        return '积分不足,请先充值';
      case 403:
        return '没有权限访问';
      case 404:
        return '请求的资源不存在';
      case 500:
        return '服务器内部错误';
      case 502:
        return '网关错误';
      case 503:
        return '服务暂时不可用';
      default:
        return '服务器错误 ($statusCode)';
    }
  }
}

2.4 重试拦截器

// lib/core/network/interceptors/retry_interceptor.dart

import 'package:dio/dio.dart';

/// 重试拦截器,自动重试失败的请求
class RetryInterceptor extends Interceptor {
  final Dio _dio;
  final int maxRetries;
  final Duration retryDelay;

  RetryInterceptor(
    this._dio, {
    this.maxRetries = 3,
    this.retryDelay = const Duration(seconds: 1),
  });

  
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    // 只重试特定类型的错误
    if (!_shouldRetry(err)) {
      return handler.next(err);
    }

    // 获取重试次数
    final retryCount = err.requestOptions.extra['retryCount'] as int? ?? 0;

    if (retryCount >= maxRetries) {
      return handler.next(err);
    }

    // 延迟后重试
    await Future.delayed(retryDelay * (retryCount + 1));

    // 更新重试次数
    err.requestOptions.extra['retryCount'] = retryCount + 1;

    try {
      final response = await _dio.fetch(err.requestOptions);
      return handler.resolve(response);
    } catch (e) {
      return handler.next(err);
    }
  }

  /// 判断是否应该重试
  bool _shouldRetry(DioException err) {
    // 超时错误重试
    if (err.type == DioExceptionType.connectionTimeout ||
        err.type == DioExceptionType.receiveTimeout) {
      return true;
    }

    // 网络错误重试
    if (err.type == DioExceptionType.connectionError) {
      return true;
    }

    // 5xx 服务器错误重试
    if (err.response?.statusCode != null &&
        err.response!.statusCode! >= 500) {
      return true;
    }

    return false;
  }
}

本篇小结(第二部分)

这部分我们完成了:

  1. ✅ TokenInterceptor(Token 注入 + 自动刷新)
  2. ✅ LogInterceptor(请求/响应日志)
  3. ✅ ErrorInterceptor(统一错误处理)
  4. ✅ RetryInterceptor(自动重试机制)

下一部分我会继续讲解 API 常量管理和错误类型定义。


三、API 常量与错误定义

3.1 API 常量管理

// lib/core/constants/api_constants.dart

/// API 常量配置
class ApiConstants {
  /// 基础 URL(根据环境切换)
  static const String baseUrl = _getBaseUrl();

  /// 开发环境
  static const String devBaseUrl = 'http://localhost:3000';

  /// 生产环境
  static const String prodBaseUrl = 'https://api.cleanmark.ai';

  /// 根据环境获取 Base URL
  static String _getBaseUrl() {
    const env = String.fromEnvironment('ENV', defaultValue: 'dev');
    return env == 'prod' ? prodBaseUrl : devBaseUrl;
  }

  // ========== 认证相关 ==========
  static const String register = '/api/auth/register';
  static const String login = '/api/auth/login';
  static const String refreshToken = '/api/auth/refresh';
  static const String logout = '/api/auth/logout';

  // ========== 用户相关 ==========
  static const String userInfo = '/api/user/me';
  static const String updateProfile = '/api/user/profile';

  // ========== 图片去水印 ==========
  static const String imageInpaint = '/api/inpaint/remove';
  static const String imageTasks = '/api/inpaint/tasks';
  static String imageTask(String id) => '/api/inpaint/tasks/$id';

  // ========== 视频去水印 ==========
  static const String videoInpaint = '/api/video-inpaint/remove';
  static const String videoTasks = '/api/video-inpaint/tasks';
  static String videoTask(String id) => '/api/video-inpaint/tasks/$id';

  // ========== 积分相关 ==========
  static const String creditHistory = '/api/credits/history';
  static const String rewardAdCredit = '/api/credits/reward-ad';
  static const String purchaseCredits = '/api/credits/purchase';

  // ========== 历史记录 ==========
  static const String allTasks = '/api/tasks';
  static String deleteTask(String id) => '/api/tasks/$id';

  // ========== 文件访问 ==========
  static String fileUrl(String filename) => '/files/output/$filename';
}

3.2 错误类型定义

// lib/core/errors/failures.dart

/// 业务失败基类
abstract class Failure {
  final String message;
  const Failure(this.message);

  
  String toString() => message;
}

/// 网络连接失败
class NetworkFailure extends Failure {
  const NetworkFailure([String message = '网络连接失败,请检查网络设置'])
      : super(message);
}

/// 服务器返回错误
class ServerFailure extends Failure {
  const ServerFailure(String message) : super(message);
}

/// 验证失败(参数错误)
class ValidationFailure extends Failure {
  const ValidationFailure(String message) : super(message);
}

/// 认证失败(未登录或 Token 过期)
class AuthFailure extends Failure {
  const AuthFailure([String message = '未授权,请重新登录']) : super(message);
}

/// 积分不足
class InsufficientCreditsFailure extends Failure {
  const InsufficientCreditsFailure([String message = '积分不足,请先充值'])
      : super(message);
}

/// 缓存读写失败
class CacheFailure extends Failure {
  const CacheFailure([String message = '本地数据读取失败,请重试'])
      : super(message);
}

/// 未知错误
class UnknownFailure extends Failure {
  const UnknownFailure(String message) : super(message);
}

3.3 统一响应模型

// lib/core/network/models/api_response.dart

/// API 统一响应模型
class ApiResponse<T> {
  final int code;
  final String message;
  final T? data;

  const ApiResponse({
    required this.code,
    required this.message,
    this.data,
  });

  factory ApiResponse.fromJson(
    Map<String, dynamic> json,
    T Function(dynamic)? fromJsonT,
  ) {
    return ApiResponse(
      code: json['code'] as int,
      message: json['message'] as String,
      data: json['data'] != null && fromJsonT != null
          ? fromJsonT(json['data'])
          : null,
    );
  }

  /// 是否成功
  bool get isSuccess => code == 200;

  /// 转换为 Either
  Either<Failure, T> toEither() {
    if (isSuccess && data != null) {
      return Right(data as T);
    } else {
      return Left(ServerFailure(message));
    }
  }
}

3.4 异常转换工具

// lib/core/network/utils/exception_handler.dart

import 'package:dio/dio.dart';
import '../../errors/failures.dart';

/// 异常处理工具类
class ExceptionHandler {
  /// 将 DioException 转换为 Failure
  static Failure handleDioException(DioException exception) {
    switch (exception.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return const NetworkFailure('网络连接超时,请检查网络设置');

      case DioExceptionType.badResponse:
        return _handleStatusCode(exception.response);

      case DioExceptionType.cancel:
        return const NetworkFailure('请求已取消');

      case DioExceptionType.connectionError:
        return const NetworkFailure('网络连接失败,请检查网络设置');

      case DioExceptionType.unknown:
        return UnknownFailure(exception.message ?? '未知错误');

      default:
        return UnknownFailure(exception.message ?? '网络请求失败');
    }
  }

  /// 处理 HTTP 状态码
  static Failure _handleStatusCode(Response? response) {
    final statusCode = response?.statusCode;
    final message = response?.data?['message'] as String?;

    switch (statusCode) {
      case 400:
        return ValidationFailure(message ?? '请求参数错误');
      case 401:
        return const AuthFailure();
      case 402:
        return const InsufficientCreditsFailure();
      case 403:
        return const ServerFailure('没有权限访问');
      case 404:
        return const ServerFailure('请求的资源不存在');
      case 500:
        return ServerFailure(message ?? '服务器内部错误');
      case 502:
        return const ServerFailure('网关错误');
      case 503:
        return const ServerFailure('服务暂时不可用');
      default:
        return ServerFailure(message ?? '服务器错误 ($statusCode)');
    }
  }

  /// 将通用异常转换为 Failure
  static Failure handleException(Object exception) {
    if (exception is DioException) {
      return handleDioException(exception);
    }
    return UnknownFailure(exception.toString());
  }
}

本篇小结(第三部分)

这部分我们完成了:

  1. ✅ API 常量管理(统一管理所有接口路径)
  2. ✅ 错误类型定义(7 种 Failure 类型)
  3. ✅ 统一响应模型(ApiResponse 封装)
  4. ✅ 异常转换工具(DioException → Failure)

下一部分我会继续讲解实际使用示例和最佳实践。


四、实际使用示例

4.1 DataSource 中使用

// lib/features/auth/data/datasources/auth_remote_datasource.dart

import 'package:dio/dio.dart';
import '../../../../core/network/api_client.dart';
import '../../../../core/network/utils/exception_handler.dart';
import '../../../../core/constants/api_constants.dart';
import '../../models/user_model.dart';

class AuthRemoteDataSource {
  final ApiClient _apiClient;

  AuthRemoteDataSource(this._apiClient);

  /// 登录
  Future<UserModel> login(String email, String password) async {
    try {
      final response = await _apiClient.post(
        ApiConstants.login,
        data: {
          'email': email,
          'password': password,
        },
      );

      return UserModel.fromJson(response.data);
    } on DioException catch (e) {
      throw ExceptionHandler.handleDioException(e);
    } catch (e) {
      throw ExceptionHandler.handleException(e);
    }
  }

  /// 注册
  Future<UserModel> register(String email, String password) async {
    try {
      final response = await _apiClient.post(
        ApiConstants.register,
        data: {
          'email': email,
          'password': password,
        },
      );

      return UserModel.fromJson(response.data);
    } on DioException catch (e) {
      throw ExceptionHandler.handleDioException(e);
    } catch (e) {
      throw ExceptionHandler.handleException(e);
    }
  }
}

4.2 Repository 中处理

// lib/features/auth/data/repositories/auth_repository_impl.dart

import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../../domain/repositories/auth_repository.dart';
import '../../domain/entities/user_entity.dart';
import '../datasources/auth_remote_datasource.dart';

class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource _remoteDataSource;

  AuthRepositoryImpl(this._remoteDataSource);

  
  Future<Either<Failure, UserEntity>> login(
    String email,
    String password,
  ) async {
    try {
      final userModel = await _remoteDataSource.login(email, password);
      return Right(userModel.toEntity());
    } on Failure catch (failure) {
      return Left(failure);
    } catch (e) {
      return Left(UnknownFailure(e.toString()));
    }
  }

  
  Future<Either<Failure, UserEntity>> register(
    String email,
    String password,
  ) async {
    try {
      final userModel = await _remoteDataSource.register(email, password);
      return Right(userModel.toEntity());
    } on Failure catch (failure) {
      return Left(failure);
    } catch (e) {
      return Left(UnknownFailure(e.toString()));
    }
  }
}

4.3 UI 层错误展示

// lib/features/auth/presentation/pages/login_page.dart

Future<void> _handleLogin() async {
  final result = await ref.read(authProvider.notifier).login(
    _emailController.text,
    _passwordController.text,
  );

  if (!mounted) return;

  result.fold(
    (failure) {
      // 根据错误类型显示不同提示
      String message;
      if (failure is NetworkFailure) {
        message = failure.message;
      } else if (failure is AuthFailure) {
        message = '邮箱或密码错误';
      } else if (failure is ValidationFailure) {
        message = failure.message;
      } else {
        message = '登录失败,请重试';
      }

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(message),
          backgroundColor: AppColors.red,
        ),
      );
    },
    (user) {
      // 登录成功,跳转首页
      context.go('/home');
    },
  );
}

五、最佳实践

5.1 环境配置

使用 --dart-define 传递环境变量:

# 开发环境
flutter run --dart-define=ENV=dev

# 生产环境
flutter run --dart-define=ENV=prod

在代码中读取:

const env = String.fromEnvironment('ENV', defaultValue: 'dev');

5.2 超时配置

// 不同接口设置不同超时时间
Future<Response> uploadFile(File file) async {
  return await _apiClient.post(
    '/api/upload',
    data: formData,
    options: Options(
      sendTimeout: const Duration(minutes: 5),  // 上传文件超时 5 分钟
    ),
  );
}

5.3 取消请求

class ImageUploadPage extends StatefulWidget {
  final CancelToken _cancelToken = CancelToken();

  
  void dispose() {
    _cancelToken.cancel('页面已关闭');
    super.dispose();
  }

  Future<void> _uploadImage() async {
    await _apiClient.post(
      '/api/upload',
      data: formData,
      cancelToken: _cancelToken,
    );
  }
}

5.4 请求去重

// lib/core/network/utils/request_cache.dart

class RequestCache {
  static final Map<String, Future<Response>> _cache = {};

  /// 带缓存的请求
  static Future<Response> cachedRequest(
    String key,
    Future<Response> Function() request,
  ) {
    if (_cache.containsKey(key)) {
      return _cache[key]!;
    }

    final future = request();
    _cache[key] = future;

    // 请求完成后清除缓存
    future.whenComplete(() => _cache.remove(key));

    return future;
  }
}

// 使用示例
Future<Response> getUserInfo() {
  return RequestCache.cachedRequest(
    'user_info',
    () => _apiClient.get('/api/user/me'),
  );
}

5.5 Mock 数据

// lib/core/network/mock/mock_interceptor.dart

class MockInterceptor extends Interceptor {
  final bool enableMock;

  MockInterceptor({this.enableMock = false});

  
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    if (!enableMock) {
      return handler.next(options);
    }

    // 根据路径返回 Mock 数据
    if (options.path == '/api/user/me') {
      return handler.resolve(
        Response(
          requestOptions: options,
          data: {
            'id': '1',
            'email': 'test@example.com',
            'credits': 10,
          },
        ),
      );
    }

    handler.next(options);
  }
}

本篇小结(第四部分)

这部分我们完成了:

  1. ✅ DataSource 使用示例
  2. ✅ Repository 错误处理
  3. ✅ UI 层错误展示
  4. ✅ 环境配置管理
  5. ✅ 超时、取消、去重、Mock 等最佳实践

下一部分我会总结本篇的关键要点。


六、完整小结

本篇完整小结

这篇文章我们完成了:

  1. ✅ 网络层架构设计(ApiClient + 拦截器链)
  2. ✅ ApiClient 封装(Dio 配置 + 基础请求方法)
  3. ✅ 四大拦截器(Token、日志、错误、重试)
  4. ✅ API 常量管理(统一管理接口路径)
  5. ✅ 错误类型定义(7 种 Failure 类型)
  6. ✅ 统一响应模型(ApiResponse 封装)
  7. ✅ 异常转换工具(DioException → Failure)
  8. ✅ 实际使用示例(DataSource、Repository、UI)
  9. ✅ 最佳实践(环境配置、超时、取消、去重、Mock)

关键要点:

架构设计

拦截器链模式:

Request
   ↓
TokenInterceptor(注入 Token)
   ↓
LogInterceptor(记录日志)
   ↓
ErrorInterceptor(统一错误)
   ↓
RetryInterceptor(自动重试)
   ↓
Network
   ↓
Response(反向经过拦截器)

职责分离:

  • ApiClient:封装 Dio,提供基础请求方法
  • Interceptors:处理横切关注点(Token、日志、错误、重试)
  • DataSource:调用 ApiClient,处理业务异常
  • Repository:转换异常为 Failure,返回 Either
  • UI:根据 Failure 类型展示友好提示

Token 管理

自动刷新机制:

  1. 请求返回 401 → Token 过期
  2. 调用 /api/auth/refresh 刷新 Token
  3. 保存新 Token 到本地
  4. 重试原请求
  5. 刷新失败 → 清除 Token,跳转登录页
if (err.response?.statusCode == 401) {
  final newToken = await _refreshToken();
  if (newToken != null) {
    // 保存新 Token
    await prefs.setString('auth_token', newToken);
    // 重试原请求
    final response = await Dio().fetch(err.requestOptions);
    return handler.resolve(response);
  }
}

错误处理策略

三层错误处理:

  1. DataSource 层:捕获 DioException,转换为业务异常
try {
  final response = await _apiClient.post(...);
  return UserModel.fromJson(response.data);
} on DioException catch (e) {
  throw ExceptionHandler.handleDioException(e);
}
  1. Repository 层:捕获业务异常,转换为 Failure
try {
  final userModel = await _remoteDataSource.login(...);
  return Right(userModel.toEntity());
} on Failure catch (failure) {
  return Left(failure);
}
  1. UI 层:根据 Failure 类型展示友好提示
result.fold(
  (failure) {
    if (failure is NetworkFailure) {
      showSnackBar('网络连接失败');
    } else if (failure is AuthFailure) {
      showSnackBar('邮箱或密码错误');
    }
  },
  (user) => navigateToHome(),
);

重试机制

指数退避策略:

// 第 1 次重试:延迟 1 秒
// 第 2 次重试:延迟 2 秒
// 第 3 次重试:延迟 3 秒
await Future.delayed(retryDelay * (retryCount + 1));

重试条件:

  • 超时错误(connectionTimeout、receiveTimeout)
  • 网络错误(connectionError)
  • 5xx 服务器错误

不重试条件:

  • 4xx 客户端错误(参数错误、未授权等)
  • 请求被取消
  • 已达最大重试次数

日志管理

开发环境:

  • 详细日志(请求头、请求体、响应体)
  • 彩色输出,便于调试

生产环境:

  • 关键日志(错误、异常)
  • 上报到日志服务(Sentry、Firebase Crashlytics)
// 根据环境配置日志级别
final logger = Logger(
  level: kDebugMode ? Level.debug : Level.error,
);

性能优化

1. 请求去重

// 相同请求正在进行时,复用 Future
RequestCache.cachedRequest('user_info', () => _apiClient.get(...));

2. 连接池复用

// Dio 默认启用连接池,无需额外配置

3. 压缩传输

// 启用 gzip 压缩
headers: {
  'Accept-Encoding': 'gzip',
}

4. 取消无用请求

// 页面销毁时取消请求
_cancelToken.cancel('页面已关闭');

测试策略

1. Mock 拦截器

// 开发阶段使用 Mock 数据
ApiClient(enableMock: true);

2. 单元测试

test('登录成功时,应返回用户实体', () async {
  when(() => mockDataSource.login(any(), any()))
      .thenAnswer((_) async => mockUserModel);

  final result = await repository.login('test@example.com', 'password');

  expect(result.isRight(), true);
});

3. 集成测试

// 使用真实 API 测试
testWidgets('登录流程测试', (tester) async {
  await tester.pumpWidget(MyApp());
  await tester.enterText(find.byKey(Key('email')), 'test@example.com');
  await tester.tap(find.byKey(Key('login_button')));
  await tester.pumpAndSettle();
  expect(find.text('登录成功'), findsOneWidget);
});

思考题

  1. 如何实现请求优先级队列?(提示:自定义拦截器 + 队列管理)
  2. Token 刷新时如何避免并发请求重复刷新?(提示:单例 + 锁机制)
  3. 如何实现离线缓存,网络恢复后自动同步?(提示:本地队列 + 网络监听)

下一篇预告:第14篇 - 性能优化与内存管理


Logo

一站式 AI 云服务平台

更多推荐