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

概述

在网络请求中,超时处理是一个非常重要的环节。当网络不稳定或服务器响应缓慢时,如果没有超时机制,应用程序可能会一直等待,导致用户界面卡死,严重影响用户体验。

Flutter 提供了多种方式来实现请求超时处理,本文将详细介绍这些方法,并提供完整的代码示例。

为什么需要超时处理

在实际开发中,网络请求可能会因为以下原因无法及时完成:

  • 网络连接不稳定
  • 服务器负载过高
  • 网络延迟过大
  • 请求的数据量过大

如果没有超时处理,这些情况会导致:

  • 用户界面卡死,无法进行任何操作
  • 应用程序消耗过多资源
  • 用户体验严重下降
  • 可能导致应用程序崩溃

因此,合理的超时处理是构建健壮网络请求的必要条件。

使用 Future.timeout() 实现超时

Dart 的 Future 类提供了 timeout() 方法,可以为异步操作设置超时时间。

基本用法

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

Future<dynamic> fetchWithTimeout(String url, {Duration timeout = const Duration(seconds: 10)}) async {
  try {
    final response = await http.get(Uri.parse(url)).timeout(timeout);
    return jsonDecode(response.body);
  } on TimeoutException catch (e) {
    throw Exception('请求超时: $e');
  } catch (e) {
    throw Exception('请求失败: $e');
  }
}

代码说明

  • timeout() 方法接收一个 Duration 参数,表示超时时间
  • 如果在指定时间内 Future 没有完成,会抛出 TimeoutException
  • 需要使用 on TimeoutException catch 来捕获超时异常
  • 其他异常仍然由普通的 catch 块处理

实际示例

try {
  var data = await fetchWithTimeout(
    'https://api.example.com/users',
    timeout: const Duration(seconds: 5),
  );
  print('请求成功: $data');
} on TimeoutException {
  print('请求超时,请稍后重试');
} catch (e) {
  print('请求失败: $e');
}

带超时配置的 API 客户端

将超时处理封装到 API 客户端中,可以统一管理超时配置。

实现代码

class TimeoutApiClient {
  final String baseUrl;
  final Duration defaultTimeout;

  TimeoutApiClient({
    required this.baseUrl,
    this.defaultTimeout = const Duration(seconds: 15),
  });

  Map<String, String> _buildHeaders() {
    return {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    };
  }

  Future<dynamic> get(String path, {Duration? timeout, Map<String, String>? query}) async {
    Uri uri = Uri.parse('$baseUrl$path');
    if (query != null) {
      uri = uri.replace(queryParameters: query);
    }

    try {
      final response = await http
          .get(uri, headers: _buildHeaders())
          .timeout(timeout ?? defaultTimeout);
      return _handleResponse(response);
    } on TimeoutException {
      throw RequestTimeoutException('GET请求超时');
    }
  }

  Future<dynamic> post(String path, Map<String, dynamic> body, {Duration? timeout}) async {
    try {
      final response = await http
          .post(
            Uri.parse('$baseUrl$path'),
            headers: _buildHeaders(),
            body: jsonEncode(body),
          )
          .timeout(timeout ?? defaultTimeout);
      return _handleResponse(response);
    } on TimeoutException {
      throw RequestTimeoutException('POST请求超时');
    }
  }

  dynamic _handleResponse(http.Response response) {
    if (response.statusCode >= 200 && response.statusCode < 300) {
      return jsonDecode(response.body);
    } else {
      throw Exception('HTTP Error: ${response.statusCode}');
    }
  }
}

使用示例

final apiClient = TimeoutApiClient(baseUrl: 'https://api.example.com');

// 使用默认超时
var user = await apiClient.get('/users/1');

// 使用自定义超时
var data = await apiClient.get('/large-data', timeout: const Duration(seconds: 30));

// POST请求
var result = await apiClient.post('/users', {'name': 'John'});

自定义超时异常

创建自定义异常类可以更好地区分不同类型的错误。

实现代码

class RequestTimeoutException implements Exception {
  final String message;
  final DateTime timestamp;
  final String? endpoint;

  RequestTimeoutException(this.message, {this.endpoint}) 
      : timestamp = DateTime.now();

  
  String toString() {
    return 'RequestTimeoutException: $message'
        '${endpoint != null ? ' (端点: $endpoint)' : ''}'
        ' (时间: ${timestamp.toIso8601String()})';
  }
}

代码说明

  • message:错误消息
  • timestamp:超时发生的时间
  • endpoint:可选的请求端点信息
  • 重写 toString() 方法提供详细的错误信息

使用示例

try {
  var data = await apiClient.get('/users');
} on RequestTimeoutException catch (e) {
  print('超时发生在: ${e.timestamp}');
  print('请求端点: ${e.endpoint}');
  // 可以根据端点信息进行不同的处理
} catch (e) {
  print('其他错误: $e');
}

不同请求类型的超时配置

不同类型的请求需要不同的超时时间,创建统一的配置类可以方便管理。

配置类实现

class TimeoutConfig {
  static const Duration short = Duration(seconds: 5);
  static const Duration normal = Duration(seconds: 10);
  static const Duration long = Duration(seconds: 30);
  static const Duration upload = Duration(seconds: 60);
  static const Duration download = Duration(seconds: 120);
}

适用场景

超时配置 时间 适用场景
short 5秒 快速查询、健康检查、简单接口
normal 10秒 普通API请求、数据列表查询
long 30秒 大数据查询、复杂计算、报表生成
upload 60秒 文件上传、表单提交
download 120秒 大文件下载

使用示例

final client = TimeoutApiClient(baseUrl: 'https://api.example.com');

// 健康检查 - 使用短超时
var health = await client.get('/health', timeout: TimeoutConfig.short);

// 用户列表 - 使用正常超时
var users = await client.get('/users', timeout: TimeoutConfig.normal);

// 生成报表 - 使用长时间超时
var report = await client.get('/report/generate', timeout: TimeoutConfig.long);

// 上传文件 - 使用上传超时
var result = await client.post('/upload', data, timeout: TimeoutConfig.upload);

超时处理的高级技巧

1. 指数退避重试

在超时后进行重试时,可以使用指数退避策略,避免频繁重试。

Future<dynamic> fetchWithRetry(String url, {
  int maxRetries = 3,
  Duration initialDelay = const Duration(seconds: 1),
}) async {
  int retryCount = 0;
  Duration delay = initialDelay;

  while (retryCount < maxRetries) {
    try {
      final response = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 10));
      return jsonDecode(response.body);
    } on TimeoutException {
      retryCount++;
      if (retryCount >= maxRetries) {
        throw RequestTimeoutException('请求超时,已重试$maxRetries次');
      }
      await Future.delayed(delay);
      delay = delay * 2;
    }
  }

  throw Exception('重试次数已用尽');
}

2. 动态超时调整

根据网络状态动态调整超时时间。

Future<dynamic> fetchWithDynamicTimeout(String url) async {
  Duration timeout = const Duration(seconds: 10);
  
  // 检查网络状态,如果是移动网络,使用更短的超时
  if (await _isMobileNetwork()) {
    timeout = const Duration(seconds: 5);
  }
  
  final response = await http.get(Uri.parse(url)).timeout(timeout);
  return jsonDecode(response.body);
}

3. 超时回调

提供超时回调函数,方便在超时时执行特定操作。

Future<dynamic> fetchWithTimeoutCallback(
  String url, {
  Duration timeout = const Duration(seconds: 10),
  void Function()? onTimeout,
}) async {
  try {
    final response = await http.get(Uri.parse(url)).timeout(timeout);
    return jsonDecode(response.body);
  } on TimeoutException {
    onTimeout?.call();
    throw RequestTimeoutException('请求超时');
  }
}

UI 层的超时处理

在 UI 层需要为用户提供友好的超时提示。

示例代码

class TimeoutDemoPage extends StatefulWidget {
  const TimeoutDemoPage({super.key});

  
  State<TimeoutDemoPage> createState() => _TimeoutDemoPageState();
}

class _TimeoutDemoPageState extends State<TimeoutDemoPage> {
  String _status = '准备就绪';
  bool _loading = false;

  Future<void> _fetchData() async {
    setState(() {
      _loading = true;
      _status = '请求中...';
    });

    try {
      final response = await http
          .get(Uri.parse('https://jsonplaceholder.typicode.com/users/1'))
          .timeout(const Duration(seconds: 5));

      var user = jsonDecode(response.body);
      setState(() {
        _status = '请求成功!\n用户名: ${user['name']}';
      });
    } on TimeoutException {
      setState(() {
        _status = '请求超时! 请检查网络连接或稍后重试';
      });
      _showTimeoutDialog();
    } catch (e) {
      setState(() {
        _status = '请求失败: $e';
      });
    } finally {
      setState(() => _loading = false);
    }
  }

  void _showTimeoutDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('请求超时'),
        content: const Text('网络请求超时,请检查网络连接后重试'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('确定'),
          ),
        ],
      ),
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('超时处理示例')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: _loading ? null : _fetchData,
              child: _loading 
                  ? const CircularProgressIndicator() 
                  : const Text('发起请求'),
            ),
            const SizedBox(height: 20),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Text(_status),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

超时处理最佳实践

1. 根据请求类型设置不同超时时间

不同类型的请求需要不同的超时配置,不能一刀切。

2. 使用自定义异常类型区分超时错误

自定义异常可以携带更多信息,方便错误处理和日志记录。

3. 在 UI 层显示友好的超时提示

用户需要知道发生了什么,以及应该怎么做。

4. 为关键请求提供重试机制

对于重要的请求,可以在超时后自动重试。

5. 监控超时率优化超时配置

通过监控超时率,不断优化超时配置,找到最佳平衡点。

6. 避免设置过短的超时导致正常请求失败

超时时间设置过短会导致正常请求也失败,需要根据实际情况调整。

7. 考虑网络状态

在移动网络下,超时时间可以适当缩短;在 Wi-Fi 下,可以适当延长。

总结

超时处理是网络请求中不可或缺的一部分,合理的超时配置可以:

  • 避免应用程序卡死
  • 提升用户体验
  • 减少资源浪费
  • 提高系统稳定性

通过本文介绍的方法,你可以构建一个健壮的超时处理机制,确保你的应用在各种网络环境下都能正常运行。

完整示例代码

以下是一个完整的超时处理示例:

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

class TimeoutApiClient {
  final String baseUrl;
  final Duration defaultTimeout;

  TimeoutApiClient({
    required this.baseUrl,
    this.defaultTimeout = const Duration(seconds: 10),
  });

  Future<dynamic> get(String path, {Duration? timeout}) async {
    try {
      final response = await http
          .get(Uri.parse('$baseUrl$path'))
          .timeout(timeout ?? defaultTimeout);
      return jsonDecode(response.body);
    } on TimeoutException {
      throw RequestTimeoutException('GET请求超时');
    }
  }

  Future<dynamic> post(String path, Map<String, dynamic> body, {Duration? timeout}) async {
    try {
      final response = await http
          .post(
            Uri.parse('$baseUrl$path'),
            headers: {'Content-Type': 'application/json'},
            body: jsonEncode(body),
          )
          .timeout(timeout ?? defaultTimeout);
      return jsonDecode(response.body);
    } on TimeoutException {
      throw RequestTimeoutException('POST请求超时');
    }
  }
}

class RequestTimeoutException implements Exception {
  final String message;
  final DateTime timestamp;

  RequestTimeoutException(this.message) : timestamp = DateTime.now();

  
  String toString() {
    return 'RequestTimeoutException: $message (${timestamp.toIso8601String()})';
  }
}

class TimeoutConfig {
  static const Duration short = Duration(seconds: 5);
  static const Duration normal = Duration(seconds: 10);
  static const Duration long = Duration(seconds: 30);
}

通过这个示例,你可以快速实现一个带有超时处理的 API 客户端,为你的网络请求提供可靠的超时保护。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐