# Flutter 项目统一错误处理规范 ## 核心原则 **所有接口异常都不应该影响页面展示,只显示友好提示弹窗(2秒后自动消失)** ## 实现方案 ### 1. Cubit/Bloc 层处理 #### ✅ 正确做法 ```dart class MyCubit extends Cubit { Future fetchData() async { try { emit(state.copyWith(isLoading: true)); final result = await useCase.call(params); result.fold( (failure) { // 🔥 设置错误信息 + 标记需要显示弹窗 emit(state.copyWith( isLoading: false, errorMessage: ErrorHandler.getErrorMessage(failure.message), shouldShowError: true, // 关键! )); }, (data) { emit(state.copyWith( isLoading: false, data: data, )); }, ); } catch (e) { // 🔥 捕获所有异常 emit(state.copyWith( isLoading: false, errorMessage: ErrorHandler.getErrorMessage(e), shouldShowError: true, // 关键! )); } } } ``` #### ❌ 错误做法 ```dart // ❌ 不要抛出异常到页面层 throw Exception('网络错误'); // ❌ 不要显示原始错误信息 emit(state.copyWith(errorMessage: e.toString())); // ❌ 不要让页面崩溃 if (error) throw error; ``` ### 2. State 设计 ```dart class MyState extends Equatable { final bool isLoading; final String? errorMessage; final bool shouldShowError; // 🔥 关键字段 const MyState({ this.isLoading = false, this.errorMessage, this.shouldShowError = false, // 默认 false }); MyState copyWith({ bool? isLoading, String? errorMessage, bool? shouldShowError, }) { return MyState( isLoading: isLoading ?? this.isLoading, errorMessage: errorMessage, shouldShowError: shouldShowError ?? false, // 🔥 默认重置为 false ); } } ``` **关键点:** - `shouldShowError` 默认为 `false` - 每次 emit 时如果不传,会自动重置为 `false` - 这样可以确保错误弹窗只显示一次 ### 3. 页面层监听 ```dart BlocConsumer( listener: (context, state) { // 🔥 自动显示错误弹窗 if (state.shouldShowError && state.errorMessage != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.errorMessage!), backgroundColor: Colors.orange, duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, ), ); } }, builder: (context, state) { // 页面正常渲染,不受错误影响 if (state.isLoading) { return const CircularProgressIndicator(); } return YourContent(); }, ) ``` ### 4. ErrorHandler 工具类 ```dart import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; class ErrorHandler { /// 获取友好的错误消息 static String getErrorMessage(Object error) { if (error is DioException) { switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return '网络连接超时,请检查网络设置'; case DioExceptionType.connectionError: return '网络连接失败,请检查网络'; case DioExceptionType.badResponse: final statusCode = error.response?.statusCode; if (statusCode == 401) { return '登录已过期,请重新登录'; } else if (statusCode == 403) { return '没有权限执行此操作'; } else if (statusCode == 404) { return '请求的资源不存在'; } else if (statusCode == 500) { return '服务器异常,请稍后重试'; } else { return '服务器响应异常'; } default: return '请求失败,请稍后重试'; } } else { return '操作失败,请稍后重试'; } } } ``` ## 完整示例 ### DeviceTaskCubit 示例 ```dart class DeviceTaskCubit extends Cubit { Future cancelTask(String deviceId) async { final taskId = state.currentTaskId; if (taskId == null) { emit(state.copyWith(errorMessage: '无可用任务')); return; } emit(state.copyWith( isLoading: true, operationType: DeviceTaskOperationType.cancel, )); try { final result = await _cancelTaskUseCase.call(params); result.fold( (failure) { _logger.logWithLevel('❌ 取消任务失败: ${failure.message}'); emit(state.copyWith( isLoading: false, errorMessage: ErrorHandler.getErrorMessage(failure.message), operationType: DeviceTaskOperationType.none, shouldShowError: true, // 🔥 标记需要显示弹窗 )); }, (success) { _logger.logWithLevel('✅ 取消任务成功'); emit(state.copyWith( isLoading: false, operationType: DeviceTaskOperationType.none, )); }, ); } catch (e) { _logger.logWithLevel('❌ 取消任务异常: $e'); emit(state.copyWith( isLoading: false, errorMessage: ErrorHandler.getErrorMessage(e), operationType: DeviceTaskOperationType.none, shouldShowError: true, // 🔥 标记需要显示弹窗 )); } } } ``` ### 页面使用示例 ```dart class TaskPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (_) => GetIt.I(), child: Scaffold( appBar: AppBar(title: const Text('任务管理')), body: BlocConsumer( listener: (context, state) { // 🔥 自动显示错误弹窗 if (state.shouldShowError && state.errorMessage != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.errorMessage!), backgroundColor: Colors.orange, duration: const Duration(seconds: 2), ), ); } }, builder: (context, state) { return Column( children: [ // 页面内容不受错误影响 Text('当前任务ID: ${state.currentTaskId ?? "无"}'), ElevatedButton( onPressed: () { context.read().cancelTask(deviceId); }, child: const Text('取消任务'), ), if (state.isLoading) const CircularProgressIndicator(), ], ); }, ), ), ); } } ``` ## 错误提示文案规范 | 错误类型 | 提示文案 | |---------|---------| | 网络超时 | "网络连接超时,请检查网络设置" | | 连接失败 | "网络连接失败,请检查网络" | | 401 未授权 | "登录已过期,请重新登录" | | 403 禁止访问 | "没有权限执行此操作" | | 404 资源不存在 | "请求的资源不存在" | | 500 服务器错误 | "服务器异常,请稍后重试" | | 其他业务错误 | "操作失败,请稍后重试" | | 未知错误 | "未知错误,请稍后重试" | **注意:** - ✅ 使用友好的中文提示 - ❌ 不要显示 HTTP 状态码 - ❌ 不要显示技术术语(如 "DioException"、"timeout") - ❌ 不要显示完整的错误堆栈 ## 优势 1. **页面不崩溃** - 所有异常都被捕获 2. **用户体验好** - 友好的中文提示 3. **自动消失** - 2秒后弹窗自动关闭 4. **不影响操作** - 用户可以继续使用页面 5. **统一规范** - 全项目统一的错误处理方式 ## 检查清单 在开发新功能时,确保: - [ ] Cubit 中所有 try-catch 都使用了 `ErrorHandler.getErrorMessage()` - [ ] State 中有 `shouldShowError` 字段 - [ ] 错误时设置 `shouldShowError: true` - [ ] copyWith 中 `shouldShowError` 默认为 `false` - [ ] 页面 BlocConsumer listener 中监听 `shouldShowError` - [ ] 使用 SnackBar 显示错误(2秒自动消失) - [ ] 不显示原始错误信息