3、请求封装思想:为什么需要封装?分层架构设计、单例模式在网络层中的应用
说实话,我见过太多 Flutter 项目,网络请求代码写得跟蜘蛛网似的。到处是 http.get、dio.post,改个 base URL 得翻遍整个项目。嗯,这其实就是典型的「没有封装」的后果。
今天我们就来聊聊,为什么网络层一定要封装?以及怎么封装才靠谱。
3.1 为什么需要封装?
先问大家一个问题:如果你的项目里,每个页面都直接调用 Dio 或 http 库,会发生什么?
我经历过一个真实案例。有个项目上线后,后端突然把接口域名从 api.old.com 改成了 api.new.com。你猜怎么着?我们三个开发改了整整两天,因为到处硬编码了域名。这还算好的,更惨的是有些地方漏改了,线上直接崩了。
封装的核心目的,说白了就三点:
- 隔离变化:底层库换了,上层不用动
- 统一管理:拦截器、超时时间、错误处理,一个地方搞定
- 提升效率:团队协作时,大家按统一规范写请求
核心原则:上层代码永远不要直接依赖第三方库。你依赖的应该是自己定义的抽象接口。
3.2 分层架构设计
我个人习惯把网络层拆成三层。你想想看,这样每层各司其职,出了问题也容易定位。
| 层级 | 职责 | 示例 |
|---|---|---|
| 数据层(Data Layer) | 封装具体请求实现,调用 Dio/http | HttpClient 类 |
| 仓库层(Repository Layer) | 数据来源决策,缓存策略 | UserRepository 类 |
| 业务层(Business Layer) | 调用仓库,处理业务逻辑 | UserBloc 或 ViewModel |
我建议你从数据层开始封装。先写一个 HttpClient 类,把 Dio 实例藏起来。
class HttpClient {
late final Dio _dio;
HttpClient() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
));
_dio.interceptors.add(LogInterceptor());
_dio.interceptors.add(AuthInterceptor());
}
Future<Response> get(String path, {Map<String, dynamic>? params}) {
return _dio.get(path, queryParameters: params);
}
Future<Response> post(String path, {dynamic data}) {
return _dio.post(path, data: data);
}
}
你看,这样写完之后,业务层根本不知道底层用的是 Dio 还是 http。哪天想换库了,只改这一个文件就行。
小技巧:我习惯在仓库层再加一层抽象。比如定义一个 UserRepository 接口,然后实现 UserRepositoryImpl。这样测试的时候可以轻松 mock。
3.3 单例模式在网络层中的应用
为什么要用单例?说白了,一个项目里只需要一个 Dio 实例就够了。你想想看,如果每个页面都 new 一个 Dio,那拦截器、token 刷新这些逻辑怎么统一管理?
我曾经接手过一个项目,里面竟然有 5 个不同的 Dio 实例。每个实例的配置还不一样,有的超时 5 秒,有的超时 30 秒。排查问题的时候,我差点把电脑砸了。
单例模式实现起来很简单。我个人喜欢用 Dart 的 factory 构造函数:
class HttpClient {
static HttpClient? _instance;
late final Dio _dio;
HttpClient._internal() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
));
_dio.interceptors.add(AuthInterceptor());
}
factory HttpClient() {
_instance ??= HttpClient._internal();
return _instance!;
}
Future<Response> get(String path, {Map<String, dynamic>? params}) {
return _dio.get(path, queryParameters: params);
}
}
这样写的好处很明显:
- 全局唯一:整个 App 只有一个 Dio 实例
- 配置统一:拦截器、超时时间、base URL 都在一处配置
- 节省资源:不用反复创建和销毁连接
注意:单例模式虽然好用,但别滥用。我见过有人把整个网络层都写成单例,结果想切换测试环境和生产环境时,还得重启 App。正确的做法是:Dio 实例用单例,但配置参数可以动态修改。
3.4 避坑指南
最后分享几个我踩过的坑:
- 不要直接返回 Dio 的 Response 对象。我建议在数据层做一次转换,返回自己定义的
ApiResponse模型。这样上层代码跟 Dio 彻底解耦。 - 错误处理要统一。我曾经在 10 个页面里写了 10 种不同的错误提示方式。后来统一封装了一个
ApiException类,所有错误都走同一个处理流程。 - 别忘了取消请求。页面销毁时,如果请求还在进行,可能会引发内存泄漏。我习惯用
CancelToken来管理。
嗯,封装这件事,说白了就是「把复杂留给自己,把简单留给别人」。你前期多花点心思,后期维护的时候就能少掉点头发。
下一节我们聊聊具体的拦截器实现,包括 token 自动刷新、日志打印这些实战内容。到时候我会拿我踩过的坑给大家当反面教材,哈哈。