Files
feature-next-arch/docs/ERROR_HANDLING_BEST_PRACTICE.md
Songzex 71aea48d9f 集成接口 开始执行任务的领域层和数据层和UI层的开发啊(待测试)
集成功能 暂停 取消功能 恢复功能的接口的领域层和数据层的开发 下一步待集成到页面上
优化功能,优化了接口异常和未知异常对页面渲染的影响对用户的体验的不好情况。具体通过弹窗友好提示!
优化更新了tcp指示灯点击出现机器状态中添加选中设备的编号 方便用户使用的明白明了!。
优化更新关闭了tcp重连操作内链条中的获取用户设备和切换设备操作项。
调整了获取无人机状态信息的更新频率 为14秒一次
2026-06-16 17:34:00 +08:00

8.3 KiB
Raw Blame History

Flutter 项目统一错误处理规范

核心原则

所有接口异常都不应该影响页面展示,只显示友好提示弹窗(2秒后自动消失)

实现方案

1. Cubit/Bloc 层处理

✅ 正确做法

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, // 关键!
      ));
    }
  }
}

❌ 错误做法

// ❌ 不要抛出异常到页面层
throw Exception('网络错误');

// ❌ 不要显示原始错误信息
emit(state.copyWith(errorMessage: e.toString()));

// ❌ 不要让页面崩溃
if (error) throw error;

2. State 设计

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. 页面层监听

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 工具类

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 示例

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, // 🔥 标记需要显示弹窗
      ));
    }
  }
}

页面使用示例

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秒自动消失)
  • 不显示原始错误信息