Skip to content

网络层最佳实践

在 Flu CLI 生成的项目中,网络层不仅仅是一个 HTTP 客户端,更是一套完整的 API 交互解决方案。本文将介绍如何高效、优雅地使用这套网络架构。

1. 核心原则

依赖注入 (Dependency Injection)

在编写 Service 时,永远不要直接实例化 AppHttp,而是应该通过构造函数注入。这对于单元测试至关重要。

推荐写法

dart
class UserService {
  final AppHttp _http;

  // 允许外部注入 Mock 的 AppHttp
  UserService({AppHttp? http}) : _http = http ?? AppHttp();
}

避免写法

dart
class UserService {
  // 强耦合,无法测试
  final AppHttp _http = AppHttp();
}

响应数据解包

AppResponse<T> 是统一的响应壳,Service 层应该负责"解包",只将业务数据(Data)或业务对象返回给 ViewModel。

推荐写法

dart
Future<User> getUserProfile() async {
  final response = await _http.get('/profile');
  if (response.isSuccess) {
    // Service 层负责转模型
    return User.fromJson(response.data);
  }
  throw ApiException(response.code, response.message);
}

2. Mock 数据的使用策略

Mock 数据是前后端分离开发的神器。Flu CLI 的网络层内置了对 Mock 的原生支持。

开启 Mock

lib/config/app_config.dart (或类似配置入口) 中:

dart
// 只有在开发环境才开启
if (!kReleaseMode) {
  AppConfig.I.useMockData = true;
}

编写 Mock 逻辑

在 Service 中,你可以优雅地处理 Mock 分支:

dart
Future<List<Product>> getProducts() async {
  // 1. Mock 分支
  if (AppConfig.I.useMockData) {
    // 模拟网络延迟,体验更真实
    await Future.delayed(const Duration(milliseconds: 500));
    return [
      Product(id: 1, name: 'Mock Product A'),
      Product(id: 2, name: 'Mock Product B'),
    ];
  }

  // 2. 真实网络请求
  final response = await _http.get('/products');
  // ... 处理响应
}

3. 拦截器的高级用法

拦截器 (lib/core/network/interceptors) 是处理全局逻辑的最佳场所。

Token 自动注入

AuthInterceptor 中:

dart
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
  final token = UserManager.I.token;
  if (token.isNotEmpty) {
    options.headers['Authorization'] = 'Bearer $token';
  }
  handler.next(options);
}

全局错误处理

有时候后端会返回 200 OK,但业务 Code 是错误码(如 401 Token 失效)。你应该在 ErrorInterceptor 或响应拦截器中统一处理。

dart
// 伪代码示例
if (response.data['code'] == 401) {
  // 触发全局登出事件
  EventBus.emit(LogoutEvent());
  // 抛出特定异常,中断后续逻辑
  throw UnauthenticatedException();
}

4. 多环境配置

不要将 BaseURL 硬编码在代码中。推荐使用 Config 类配合 flutter build --dart-define 或编译配置。

dart
class EnvConfig {
  static const appName = String.fromEnvironment('APP_NAME', defaultValue: 'Flu App');
  static const baseUrl = String.fromEnvironment('BASE_URL', defaultValue: 'https://api.dev.com');
}

// 初始化
AppConfig.I.init(baseUrl: EnvConfig.baseUrl);

5. 常见误区 FAQ

Q: 为什么我的 Service 返回的是 AppResponse 而不是业务对象? A: 这是一个常见误区。ViewModel 不应该感知 HTTP 协议细节(如 code, message)。Service 应该消化掉 AppResponse,只吐出 User, List<Product> 这样的纯业务对象。

Q: 如何上传文件? A: Dio 原生支持 FormData。

dart
final formData = FormData.fromMap({
  'file': await MultipartFile.fromFile('./text.txt', filename: 'upload.txt'),
});
await _http.post('/upload', data: formData);

Q: 如何取消请求? A: 使用 CancelToken。页面销毁时记得取消未完成的请求,防止内存泄漏。

Released under the MIT License.