3、请求封装思想:为什么需要封装?分层架构设计、单例模式在网络层中的应用

说实话,我见过太多 Flutter 项目,网络请求代码写得跟蜘蛛网似的。到处是 http.getdio.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) 调用仓库,处理业务逻辑 UserBlocViewModel

我建议你从数据层开始封装。先写一个 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 自动刷新、日志打印这些实战内容。到时候我会拿我踩过的坑给大家当反面教材,哈哈。