Files
flutterApp/docs/ERROR_HANDLING_BEST_PRACTICE.md

304 lines
8.3 KiB
Markdown
Raw Normal View History

# Flutter 项目统一错误处理规范
## 核心原则
**所有接口异常都不应该影响页面展示,只显示友好提示弹窗(2秒后自动消失)**
## 实现方案
### 1. Cubit/Bloc 层处理
#### ✅ 正确做法
```dart
class MyCubit extends Cubit<MyState> {
Future<void> 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<MyCubit, MyState>(
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<DeviceTaskState> {
Future<void> 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<DeviceTaskCubit>(),
child: Scaffold(
appBar: AppBar(title: const Text('任务管理')),
body: BlocConsumer<DeviceTaskCubit, DeviceTaskState>(
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<DeviceTaskCubit>().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秒自动消失)
- [ ] 不显示原始错误信息