让 Flutter API 请求变得简单:http vs Dio — 你该如何选择?

这是一份在 Flutter 中获取数据的实用指南,对比了 http 和 Dio,并帮助你根据应用需求选择最合适的软件包。

请关注我的公众号:OpenFlutter,谢谢。

经过 3 年多的 Flutter 应用开发,我看到无数开发者都在一个看似简单的问题上苦苦挣扎:"我的 API 请求到底该用 http 包还是 Dio?"答案并不像你想象的那么简单,错误的选择可能会对你应用的性能、可维护性和用户体验产生重大影响。

在这份全面的指南中,我将分享多年生产实践中的经验洞察,揭示这两个包鲜为人知的功能,并为你提供一个决策框架,它将超越你在其他地方能找到的"Dio 功能更多"这种笼统的建议。

基础:理解引擎盖下到底是什么

在深入比较之前,让我们先了解一个大多数开发者没有意识到的事实:这两个包最终使用的是同一个底层 HTTP 客户端 。http 包使用 dart:io 的 HttpClient,而 Dio 也是如此(尽管它可以被配置为使用其他适配器)。

这意味着性能差异并不在于网络层------而在于每个包如何处理、转换和管理你的请求与响应。

http 包:具有欺骗性的简洁

http 包的表面看起来很简单,但这种简单性既是优点也是缺点。

优点:可以扩展的极简主义

dart 复制代码
import 'package:http/http.dart' as http;

Future<User> fetchUser(int id) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/users/$id'),
    headers: {'Authorization': 'Bearer $token'},
  );

  if (response.statusCode == 200) {
    return User.fromJson(jsonDecode(response.body));
  }
  throw Exception('Failed to load user');
}

让它如此强大的,是它的可预测性。每一个 HTTP 请求都遵循相同的模式,这让团队更容易在大型代码库中保持一致的代码风格。

隐藏的复杂性:大多数人不知道的

这里有一个可能会让你惊讶的事实:http 包内置了连接池和 keep-alive 功能,但许多开发者并没有利用到这一点:

dart 复制代码
import 'package:http/http.dart' as http;

class ApiClient {
  static final _client = http.Client();

  // 复用同一个客户端以实现连接池
  static Future<http.Response> get(String url) {
    return _client.get(Uri.parse(url));
  }

  // 使用完毕时别忘了关闭
  static void dispose() {
    _client.close();
  }
}

大多数开发者每次请求都创建一个新的客户端,从而错过了显著的性能优势。一个单一的 http.Client 可以通过连接复用来高效地处理成千上万个请求。

大规模应用时出现的痛点**

到处都是手动错误处理

dart 复制代码
// 这在你的整个应用中会变得重复且繁琐
if (response.statusCode >= 200 && response.statusCode < 300) {
// 成功
} else if (response.statusCode == 401) {
// 处理鉴权
} else if (response.statusCode >= 500) {
// 处理服务器错误
}

没有内置的重试

dart 复制代码
// 你需要到处写这段逻辑
Future<T> _retryRequest<T>(Future<T> Function() request) async {
  for (int i = 0; i < 3; i++) {
    try {
      return await request();
    } catch (e) {
      if (i == 2) rethrow;
      await Future.delayed(Duration(seconds: pow(2, i).toInt()));
    }
  }
  throw Exception('Max retries exceeded');
}

Dio:一把有隐藏成本的瑞士军刀

Dio 将自己宣传为"功能强大的 Dart HTTP 客户端",它兑现了这一承诺------有时甚至做得过头了。

强大之处:解决实际问题的功能

dart 复制代码
import 'package:dio/dio.dart';

final dio = Dio(BaseOptions(
  baseUrl: 'https://api.example.com',
  connectTimeout: const Duration(seconds: 5),
  receiveTimeout: const Duration(seconds: 3),
  headers: {'Content-Type': 'application/json'},
));

// 自动地为请求/响应进行数据转换
Future<User> fetchUser(int id) async {
  final response = await dio.get('/users/$id');
  return User.fromJson(response.data);
}

真正改变游戏规则的功能:大多数开发者没有充分利用的

复杂的拦截器

这是我在生产应用中使用的一个模式,可以自动处理令牌刷新:

dart 复制代码
class AuthInterceptor extends Interceptor {
  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      try {
        await _refreshToken();
        // 重试原来的网络请求
        final clonedRequest = await dio.fetch(err.requestOptions);
        return handler.resolve(clonedRequest);
      } catch (e) {
        // Redirect to login
        return handler.next(err);
      }
    }
    handler.next(err);
  }
}

内置缓存与自定义策略

dart 复制代码
dio.interceptors.add(DioCacheInterceptor(
  options: CacheOptions(
    store: MemCacheStore(),
    policy: CachePolicy.forceCache,
    maxStale: const Duration(hours: 1),
  ),
));

对上传进度进行细粒度控制

dart 复制代码
await dio.post(
  '/upload',
  data: FormData.fromMap({'file': await MultipartFile.fromFile(path)}),
  onSendProgress: (sent, total) {
    print('Upload: ${(sent / total * 100).toStringAsFixed(0)}%');
  },
);

隐藏的成本:文档没有告诉你的

  1. 内存占用

Dio 的功能丰富性是有代价的。在我的基准测试中,一个基本的 Dio 设置在处理简单请求时,比 http 包多占用大约 3 倍的内存。这在内存受限的环境中非常重要。

  1. 学习曲线的复杂性

    Dio 的灵活性可能导致过度设计。我见过团队花几周时间调试拦截器链,而这些问题原本用简单的函数就能解决。

  2. 依赖包的重量

    Dio 引入了更多的依赖项,这会增加你的应用的构建时间和潜在的依赖冲突。

真实世界的决策矩阵

在处理了数十个生产应用中的这两个包之后,这是我的决策框架:

在以下情况选择 http:

  • 简单的 API 调用(REST 风格的简单CRUD)
  • 团队中有初级开发者(更容易理解和调试)
  • 性能敏感型应用(内存占用更低)
  • 库/包开发(依赖项更少)
  • 微服务架构(不同的服务可能需要不同的配置)

在以下情况选择 Dio:

  • 复杂的认证流程(令牌刷新、多种认证方式)
  • 文件上传/下载,并需要进度跟踪
  • 贯穿整个应用的复杂错误处理
  • 频繁的 API 交互(拦截器减少样板代码)
  • GraphQL 或自定义协议(通过适配器)

混合方法:鱼与熊掌兼得

这是我在大型应用中使用的一个高级模式:

csharp 复制代码
abstract class ApiClient {
  Future<T> get<T>(String path);
  Future<T> post<T>(String path, dynamic data);
}

class SimpleApiClient implements ApiClient {
  final http.Client _client = http.Client();

  @override
  Future<T> get<T>(String path) async {
    // 简单 http 实现
  }
}
class AdvancedApiClient implements ApiClient {
  final Dio _dio = Dio();

  @override
  Future<T> get<T>(String path) async {
    // 带拦截器的Dio 实现 
  }
}
// Use factory pattern to choose based on context
class ApiClientFactory {
  static ApiClient create(ApiComplexity complexity) {
    return complexity == ApiComplexity.simple
        ? SimpleApiClient()
        : AdvancedApiClient();
  }
}

性能基准测试:重要的数据

根据我在各种场景下的测试: 好的,这是这段内容的中文翻译,并以 Markdown 表格形式呈现:

场景 HTTP 包 Dio 胜出者
简单的 GET 请求 (1000 次) 2.3s 2.8s HTTP
带拦截器的 GET 请求 不适用 3.1s -
内存占用 (空闲时) 15MB 45MB HTTP
包体积影响 +0.2MB +1.8MB HTTP
开发速度 基准 快 40% Dio

结论:关键在于上下文,而不是功能

"哪个更好"这个问题没有抓住重点。正确的选择取决于你的具体上下文:

  • 对于 MVP 和简单应用:从 http 开始。你随时可以稍后迁移。
  • 对于复杂、API 调用频繁的应用:Dio 的功能会为你节省数周的开发时间。
  • 对于库的开发者:http 的最小化占用尊重了你用户的依赖树。
  • 对于团队经验水平参差不齐的团队:http 的显式性使得代码审查更容易。

好的,这是这段内容的中文翻译:

http 包高级用户技巧

dart 复制代码
// 创建一个具有优化设置的自定义客户端
final client = http.Client();
// 很多开发者并不知道这个
client.connectionTimeout = Duration(seconds: 30);

真正重要的 Dio 配置

dart 复制代码
// 这个配置为我省去了无数生产环境中的问题
final dio = Dio(BaseOptions(
  connectTimeout: Duration(seconds: 5),
  sendTimeout: Duration(seconds: 30),
  receiveTimeout: Duration(seconds: 30),
  // Critical for mobile networks
  followRedirects: true,
  maxRedirects: 3,
));

结论:这是一个工程决策

经过 3 年多的 Flutter 开发,我明白了一个道理:最好的技术决策并不总是功能最多的那个------而是为你的特定上下文选择合适的那个。

这两个包都是优秀的工具。http 包的简洁性不是一个局限;它是一种设计选择,旨在促进显式、可维护的代码。

Dio 的功能丰富性不是臃肿;它是一个针对复杂网络需求的综合解决方案。

关键在于理解你的需求、你的团队能力以及你的应用的长期演进。从简单开始,在需要时扩展复杂性,并始终将可维护性置于巧妙之上。

记住:最好的 API 客户端,是你的整个团队都能有效理解、调试和维护的那个。

相关推荐
剪刀石头布啊6 分钟前
泛洪DFS、BFS
前端
qetfw6 分钟前
Windows Server AD CS:根 CA、Web 证书模板与域内自动注册
前端·windows·windows-server
剪刀石头布啊6 分钟前
为什么React组件很少前缀,而vue的不少组件都有前缀
前端
剪刀石头布啊12 分钟前
gird网格布局
前端
啃火龙果的兔子14 分钟前
Google Chrome(谷歌浏览器)常用快捷键
前端·chrome
剪刀石头布啊16 分钟前
js真的是单线程实现异步并发么?
前端
剪刀石头布啊21 分钟前
js中改变this指向的操作有哪些
前端
剪刀石头布啊21 分钟前
vue、react 列表中使用 index 作为 key 的话,修改某一个元素后会发生什么
前端
方方洛23 分钟前
glowglow:语言无关的语法高亮器是怎么实现的
前端·前端框架
java_nnnn1 小时前
JavaEE进阶-HTML基础认识
前端·java-ee·html