304 lines
8.3 KiB
Markdown
304 lines
8.3 KiB
Markdown
|
|
# 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秒自动消失)
|
|||
|
|
- [ ] 不显示原始错误信息
|