在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

概述

网络请求是应用中最容易出错的环节之一。网络不稳定、服务器故障、参数错误等都可能导致请求失败。一个健壮的应用必须能够全面捕获和处理各种错误情况,为用户提供良好的体验。

本章将详细介绍Flutter网络请求中的错误捕获与处理方法,包括错误类型分类、基础错误处理、分类错误处理、自定义异常类以及统一错误处理策略。


1. 错误类型分类

网络请求中可能遇到的错误可以分为以下几类:

错误类型 说明 处理方式
SocketException 网络连接失败,无法连接到服务器 检查网络状态,提示用户
HttpException HTTP协议错误,URL格式不正确等 检查URL格式
FormatException 数据格式错误,JSON解析失败等 检查响应格式,验证数据
TimeoutException 请求超时,服务器未在规定时间内响应 设置超时时间,实现重试
4xx状态码 客户端错误,请求参数错误或未授权 检查请求参数,提示用户
5xx状态码 服务器错误,服务器内部故障 重试请求或提示用户稍后重试

1.1 错误分类流程图

网络请求错误分类
├── 异常错误(Exception)
│   ├── SocketException: 网络连接失败
│   ├── HttpException: HTTP协议错误
│   ├── FormatException: 数据格式错误
│   └── TimeoutException: 请求超时
└── 状态码错误(StatusCode)
    ├── 4xx: 客户端错误
    │   ├── 400: 请求参数错误
    │   ├── 401: 未授权
    │   ├── 403: 禁止访问
    │   └── 404: 资源不存在
    └── 5xx: 服务器错误
        ├── 500: 服务器内部错误
        ├── 502: 网关错误
        └── 503: 服务不可用

2. 基础错误处理

基础错误处理使用try-catch包裹网络请求,捕获所有可能的异常。

2.1 最简单的错误处理

import 'package:http/http.dart' as http;

Future<void> basicErrorHandling() async {
  try {
    final response = await http.get(Uri.parse('https://api.example.com/data'));

    if (response.statusCode == 200) {
      print('请求成功');
    } else {
      print('服务器返回错误: ${response.statusCode}');
    }
  } catch (e) {
    print('网络请求失败: $e');
  }
}

2.2 基础错误处理的问题

这种方式虽然简单,但存在以下问题:

  1. 无法区分错误类型:所有异常都被同一个catch块捕获
  2. 错误信息不明确:用户无法知道具体是什么错误
  3. 处理方式单一:无法根据不同错误类型采取不同策略

3. 分类错误处理

分类错误处理针对不同的异常类型和状态码进行差异化处理。

3.1 按异常类型捕获

import 'dart:io';

Future<void> detailedErrorHandling() async {
  try {
    final response = await http.get(Uri.parse('https://api.example.com/data'));

    if (response.statusCode >= 200 && response.statusCode < 300) {
      print('请求成功');
    } else if (response.statusCode == 400) {
      print('请求参数错误');
    } else if (response.statusCode == 401) {
      print('未授权,请重新登录');
    } else if (response.statusCode == 403) {
      print('禁止访问');
    } else if (response.statusCode == 404) {
      print('资源不存在');
    } else if (response.statusCode >= 500) {
      print('服务器错误,请稍后重试');
    }
  } on SocketException {
    print('网络连接失败,请检查网络');
  } on HttpException {
    print('HTTP协议错误');
  } on FormatException {
    print('数据格式错误');
  } catch (e) {
    print('未知错误: $e');
  }
}

3.2 分类处理的优势

  1. 精确识别错误类型:可以知道是网络问题、协议问题还是数据格式问题
  2. 针对性处理:不同错误类型采取不同的处理策略
  3. 友好提示:为用户提供更准确的错误信息

4. 自定义异常类

为了更好地管理错误,我们可以定义自定义异常类。

4.1 错误类型枚举

enum ApiErrorType {
  network,           // 网络错误
  unauthorized,      // 未授权
  forbidden,         // 禁止访问
  notFound,          // 资源不存在
  server,            // 服务器错误
  timeout,           // 请求超时
  unknown,           // 未知错误
}

4.2 自定义异常类

class ApiException implements Exception {
  final ApiErrorType type;
  final String message;
  final int? statusCode;

  ApiException({
    required this.type,
    required this.message,
    this.statusCode,
  });

  
  String toString() {
    return 'ApiException(type: $type, message: $message, statusCode: $statusCode)';
  }
}

4.3 使用自定义异常

Future<void> fetchData() async {
  try {
    final response = await http.get(Uri.parse('https://api.example.com/data'));

    if (response.statusCode >= 200 && response.statusCode < 300) {
      print('请求成功');
    } else {
      throw _createApiException(response.statusCode);
    }
  } on SocketException {
    throw ApiException(
      type: ApiErrorType.network,
      message: '网络连接失败',
    );
  } catch (e) {
    if (e is ApiException) {
      print('API错误: ${e.type}, ${e.message}');
    } else {
      throw ApiException(
        type: ApiErrorType.unknown,
        message: e.toString(),
      );
    }
  }
}

ApiException _createApiException(int statusCode) {
  switch (statusCode) {
    case 401:
      return ApiException(
        type: ApiErrorType.unauthorized,
        message: '未授权,请重新登录',
        statusCode: statusCode,
      );
    case 403:
      return ApiException(
        type: ApiErrorType.forbidden,
        message: '禁止访问',
        statusCode: statusCode,
      );
    case 404:
      return ApiException(
        type: ApiErrorType.notFound,
        message: '资源不存在',
        statusCode: statusCode,
      );
    case >= 500:
      return ApiException(
        type: ApiErrorType.server,
        message: '服务器错误,请稍后重试',
        statusCode: statusCode,
      );
    default:
      return ApiException(
        type: ApiErrorType.unknown,
        message: '未知错误',
        statusCode: statusCode,
      );
  }
}

5. 统一错误处理

统一错误处理将错误处理逻辑集中到一个函数中,避免代码重复。

5.1 统一错误处理函数

void handleApiError(dynamic error) {
  if (error is ApiException) {
    switch (error.type) {
      case ApiErrorType.network:
        print('网络错误: ${error.message}');
        // 显示网络错误提示
        break;
      case ApiErrorType.unauthorized:
        print('未授权: ${error.message}');
        // 跳转到登录页面
        break;
      case ApiErrorType.forbidden:
        print('禁止访问: ${error.message}');
        // 显示权限不足提示
        break;
      case ApiErrorType.notFound:
        print('资源不存在: ${error.message}');
        // 显示404页面
        break;
      case ApiErrorType.server:
        print('服务器错误: ${error.message}');
        // 显示重试按钮
        break;
      case ApiErrorType.timeout:
        print('请求超时: ${error.message}');
        // 显示超时提示,提供重试
        break;
      case ApiErrorType.unknown:
        print('未知错误: ${error.message}');
        // 显示通用错误提示
        break;
    }
  } else {
    print('错误: $error');
    // 显示通用错误提示
  }
}

5.2 在UI中使用统一错误处理

Future<void> fetchData() async {
  setState(() => _loading = true);
  
  try {
    final response = await http.get(Uri.parse('https://api.example.com/data'));
    
    if (response.statusCode == 200) {
      // 处理成功数据
    } else {
      throw _createApiException(response.statusCode);
    }
  } catch (e) {
    handleApiError(e);
  } finally {
    setState(() => _loading = false);
  }
}

6. 错误日志记录

错误日志对于排查问题至关重要,应该记录详细的错误信息。

6.1 日志记录函数

void logError({
  required String url,
  required dynamic error,
  StackTrace? stackTrace,
  Map<String, dynamic>? requestData,
}) {
  String timestamp = DateTime.now().toIso8601String();
  StringBuffer logBuffer = StringBuffer();
  
  logBuffer.writeln('[$timestamp] URL: $url');
  logBuffer.writeln('Error: $error');
  
  if (requestData != null) {
    logBuffer.writeln('Request Data: $requestData');
  }
  
  if (stackTrace != null) {
    logBuffer.writeln('StackTrace: $stackTrace');
  }
  
  print(logBuffer.toString());
  
  // 在生产环境中,可以发送到日志服务器
  // await _sendToLogServer(logBuffer.toString());
}

6.2 使用日志记录

Future<void> fetchData() async {
  try {
    final response = await http.get(Uri.parse('https://api.example.com/data'));
    
    if (response.statusCode != 200) {
      logError(
        url: 'https://api.example.com/data',
        error: 'HTTP Error: ${response.statusCode}',
      );
    }
  } catch (e, stackTrace) {
    logError(
      url: 'https://api.example.com/data',
      error: e,
      stackTrace: stackTrace,
    );
  }
}

7. 错误处理最佳实践

7.1 使用try-catch包裹所有网络请求

// 错误做法
final response = await http.get(uri);
var data = jsonDecode(response.body);

// 正确做法
try {
  final response = await http.get(uri);
  var data = jsonDecode(response.body);
} catch (e) {
  // 处理错误
}

7.2 区分网络错误和业务错误

网络错误和业务错误需要不同的处理方式:

  • 网络错误:提示用户检查网络连接
  • 业务错误:根据具体情况提示用户(如参数错误、权限不足等)

7.3 对不同状态码进行分类处理

不同的状态码代表不同的错误类型,应该进行分类处理:

  • 401:跳转到登录页面
  • 403:显示权限不足提示
  • 404:显示资源不存在页面
  • 5xx:显示服务器错误提示,提供重试按钮

7.4 使用自定义异常类统一错误类型

自定义异常类可以使错误处理更加清晰和统一。

7.5 记录详细的错误日志

在开发阶段和生产环境中都应该记录详细的错误日志,便于问题排查。

7.6 为用户提供友好的错误提示

错误提示应该简洁明了,让用户知道发生了什么问题以及如何解决。

7.7 考虑实现请求重试机制

对于网络不稳定导致的错误,可以实现重试机制,提高请求成功率。


8. 实践示例:完整的错误处理

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';

enum ApiErrorType {
  network,
  unauthorized,
  forbidden,
  notFound,
  server,
  timeout,
  unknown,
}

class ApiException implements Exception {
  final ApiErrorType type;
  final String message;
  final int? statusCode;

  ApiException({
    required this.type,
    required this.message,
    this.statusCode,
  });
}

class ApiClient {
  Future<dynamic> get(String url) async {
    try {
      final response = await http.get(Uri.parse(url));
      return _handleResponse(response);
    } on SocketException {
      throw ApiException(
        type: ApiErrorType.network,
        message: '网络连接失败,请检查网络',
      );
    } on FormatException {
      throw ApiException(
        type: ApiErrorType.unknown,
        message: '数据格式错误',
      );
    } catch (e) {
      throw ApiException(
        type: ApiErrorType.unknown,
        message: e.toString(),
      );
    }
  }

  dynamic _handleResponse(http.Response response) {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      return jsonDecode(response.body);
    } else {
      throw _createApiException(response.statusCode);
    }
  }

  ApiException _createApiException(int statusCode) {
    switch (statusCode) {
      case 401:
        return ApiException(
          type: ApiErrorType.unauthorized,
          message: '未授权,请重新登录',
          statusCode: statusCode,
        );
      case 403:
        return ApiException(
          type: ApiErrorType.forbidden,
          message: '禁止访问',
          statusCode: statusCode,
        );
      case 404:
        return ApiException(
          type: ApiErrorType.notFound,
          message: '资源不存在',
          statusCode: statusCode,
        );
      case >= 500:
        return ApiException(
          type: ApiErrorType.server,
          message: '服务器错误,请稍后重试',
          statusCode: statusCode,
        );
      default:
        return ApiException(
          type: ApiErrorType.unknown,
          message: '未知错误',
          statusCode: statusCode,
        );
    }
  }
}

void handleApiError(ApiException error) {
  switch (error.type) {
    case ApiErrorType.network:
      print('显示网络错误提示');
      break;
    case ApiErrorType.unauthorized:
      print('跳转到登录页面');
      break;
    case ApiErrorType.server:
      print('显示重试按钮');
      break;
    default:
      print('显示通用错误提示: ${error.message}');
  }
}

9. 错误处理的UI展示

在Flutter中,可以通过以下方式展示错误信息:

9.1 使用SnackBar

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: Text(error.message),
    backgroundColor: Colors.red,
  ),
);

9.2 使用Dialog

showDialog(
  context: context,
  builder: (context) => AlertDialog(
    title: const Text('错误'),
    content: Text(error.message),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context),
        child: const Text('确定'),
      ),
    ],
  ),
);

9.3 使用ErrorWidget

if (hasError) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Icon(Icons.error, color: Colors.red, size: 48),
        const SizedBox(height: 16),
        Text(error.message),
        const SizedBox(height: 16),
        ElevatedButton(
          onPressed: () => retry(),
          child: const Text('重试'),
        ),
      ],
    ),
  );
}

10. 总结

错误处理是网络编程中不可或缺的一部分:

  1. 分类处理:区分网络错误、协议错误、数据格式错误和状态码错误
  2. 自定义异常:使用自定义异常类统一错误类型
  3. 统一处理:将错误处理逻辑集中到一个函数中
  4. 日志记录:记录详细的错误信息便于排查
  5. 用户友好:为用户提供清晰的错误提示和解决方案

一个健壮的应用必须能够优雅地处理各种错误情况,为用户提供良好的体验。


参考资源

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐