基于Dio插件封装的Flutter网络请求组件2.0

48 阅读3分钟

HttpUtils - Flutter HTTP 工具 2.5

一个轻量级、功能强大的 Flutter HTTP 工具包,基于 Dio 实现,提供完整的 RESTful API 支持和 Token 管理功能。

代码地址 gitee.com/dxm_code/ht…

✨ 核心特性

  • 🎯 类型安全 - 自动类型转换,支持泛型
  • 🔄 智能认证 - 401 自动刷新 Token 并重试请求
  • 🚀 开箱即用 - 简洁的 API 设计,快速集成
  • 📦 多实例支持 - 轻松管理多个 API 服务
  • 🛡️ 并发安全 - Token 刷新机制避免竞态条件

📦 安装

1. 添加依赖

pubspec.yaml 中添加:

dependencies:
  dio: ^5.0.0

2. 添加到项目

http_utils.dartapi_response.dart 复制到项目的 utils 目录:

lib/
  └── utils/
      ├── http_utils.dart
      └── api_response.dart

🚀 基础使用

初始化

main.dart 中初始化默认实例:

void main() {
  HttpUtils.defaultInstance(
    baseUrl: 'https://api.example.com',
  );
  
  runApp(MyApp());
}

发起 GET 请求

final response = await HttpUtils().get('/users');

if (response.isSuccess) {
  print('数据: ${response.data}');
} else {
  print('错误: ${response.message}');
}

使用类型转换

// 定义模型
class User {
  final String id;
  final String name;
  
  User({required this.id, required this.name});
  
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
    );
  }
}

// 请求时指定转换函数
final response = await HttpUtils().get<User>(
  '/users/123',
  fromJson: User.fromJson,
);

if (response.isSuccess) {
  User user = response.data!;  // 类型安全
  print('用户名: ${user.name}');
}

📡 支持的请求类型

GET / POST / PUT / DELETE / PATCH

// GET
await HttpUtils().get('/users', params: {'page': 1});

// POST
await HttpUtils().post('/users', data: {'name': 'John'});

// PUT
await HttpUtils().put('/users/123', data: {'name': 'Jane'});

// DELETE
await HttpUtils().delete('/users/123');

// PATCH
await HttpUtils().patch('/users/123', data: {'name': 'Updated'});

FormData 表单提交

// 自动转换为 FormData
await HttpUtils().postForm(
  '/upload',
  formData: {
    'field1': 'value1',
    'field2': 'value2',
  },
);

文件上传

// 单文件
await HttpUtils().uploadFile(
  '/upload',
  filePath: '/path/to/image.jpg',
  fieldName: 'avatar',
  data: {'userId': '123'},
  onSendProgress: (sent, total) {
    print('进度: ${(sent / total * 100).toStringAsFixed(1)}%');
  },
);

// 多文件
await HttpUtils().uploadFiles(
  '/upload/batch',
  files: {
    'photo1': '/path/to/photo1.jpg',
    'photo2': '/path/to/photo2.jpg',
  },
);

文件下载

final response = await HttpUtils().downloadFile(
  'https://example.com/file.pdf',
  '/local/path/file.pdf',
  onReceiveProgress: (received, total) {
    print('下载: ${received}/${total}');
  },
);

🔄 类型转换器

注册单个转换器

void main() {
  HttpUtils.defaultInstance(baseUrl: 'https://api.example.com');
  
  // 注册 User 类型转换器
  HttpUtils.registerConverter<User>(User.fromJson);
  
  runApp(MyApp());
}

注册后,无需每次指定 fromJson

// 自动使用已注册的转换器
final response = await HttpUtils().get<User>('/users/123');

批量注册

HttpUtils.registerConverters({
  User: User.fromJson,
  Product: Product.fromJson,
  Order: Order.fromJson,
});

List 类型转换

// 注册后自动支持 List
HttpUtils.registerConverter<User>(User.fromJson);

// 获取用户列表
final response = await HttpUtils().get<List<User>>('/users');
if (response.isSuccess) {
  List<User> users = response.data!;
}

🔐 Token 认证

开启认证

初始化时启用认证:

HttpUtils.defaultInstance(
  baseUrl: 'https://api.example.com',
  enableAuth: true,  // 开启全局认证
);

配置 Token 生成器

决定如何将 Token 添加到请求头:

// 全局配置
HttpUtils.globalOnGenerateAuthToken = (token) {
  return {'Authorization': 'Bearer $token'};
};

// 或实例级配置(优先级更高)
httpElement.onGenerateAuthToken = (token) {
  return {
    'Authorization': 'Bearer $token',
    'X-Custom-Header': 'value',
  };
};

配置 Token 获取函数

HttpUtils().onGetToken = ([bool refresh = false]) async {
  if (refresh) {
    // 刷新 Token
    final newToken = await refreshTokenFromServer();
    await saveTokenToLocal(newToken);
    return newToken;
  }
  
  // 获取本地 Token
  return await getTokenFromLocal();
};

配置认证失败回调

HttpUtils.onAuthFailed = () {
  // Token 刷新失败,跳转登录
  navigateToLogin();
};

自动刷新机制

当请求返回 401 时,HttpUtils 会:

  1. 自动调用 onGetToken(true) 刷新 Token
  2. 等待中的请求进入队列
  3. Token 刷新成功后,自动重试所有失败请求
  4. Token 刷新失败时,触发 onAuthFailed 回调

并发安全:多个 401 响应仅触发一次刷新。

请求级别控制

// 此请求需要认证(覆盖全局配置)
await HttpUtils().get('/profile', requireAuth: true);

// 此请求不需要认证
await HttpUtils().get('/public/data', requireAuth: false);

完整示例

void main() {
  // 初始化
  HttpUtils.defaultInstance(
    baseUrl: 'https://api.example.com',
    enableAuth: true,
  );
  
  // Token 生成器
  HttpUtils.globalOnGenerateAuthToken = (token) {
    return {'Authorization': 'Bearer $token'};
  };
  
  // Token 获取
  HttpUtils().onGetToken = ([refresh = false]) async {
    final prefs = await SharedPreferences.getInstance();
    if (refresh) {
      // 刷新逻辑
      final refreshToken = prefs.getString('refresh_token');
      final response = await HttpUtils().post(
        '/auth/refresh',
        data: {'refreshToken': refreshToken},
        requireAuth: false,
      );
      final newToken = response.data['token'];
      await prefs.setString('token', newToken);
      return newToken;
    }
    return prefs.getString('token');
  };
  
  // 认证失败
  HttpUtils.onAuthFailed = () {
    Get.offAllNamed('/login');
  };
  
  runApp(MyApp());
}

🏢 多实例管理

创建命名实例

适用于需要对接多个后端服务的场景:

// 主应用 API
HttpUtils.named(
  name: 'app',
  baseUrl: 'https://app-api.example.com',
  enableAuth: true,
);

// 支付 API
HttpUtils.named(
  name: 'payment',
  baseUrl: 'https://pay-api.example.com',
  enableAuth: true,
);

// 第三方 API
HttpUtils.named(
  name: 'third-party',
  baseUrl: 'https://api.third-party.com',
  enableAuth: false,
);

使用命名实例

// 获取实例
final appApi = HttpUtils.getInstance('app')!;
final paymentApi = HttpUtils.getInstance('payment')!;

// 发起请求
await appApi.get('/user/profile');
await paymentApi.post('/orders', data: orderData);

实例管理

// 移除实例
HttpUtils.removeInstance('third-party');

// 清除所有命名实例
HttpUtils.clearAllInstances();

📖 ApiResponse 响应模型

所有请求返回统一的 ApiResponse<T>

class ApiResponse<T> {
  final bool success;        // 请求是否成功
  final int code;            // 状态码
  final String message;      // 响应消息
  final T? data;             // 业务数据
}

便捷属性

response.isSuccess  // 是否成功
response.isError    // 是否失败
response.hasData    // 是否有数据

支持的响应格式

HttpUtils 自动适配多种 JSON 格式:

{"success": true, "code": 200, "message": "成功", "data": {...}}
{"status": 200, "msg": "OK", "list": [...]}
{"code": 200, "message": "Success", "info": {...}}

❓ 常见问题

Q: 忘记初始化直接使用 HttpUtils() 怎么办?

会抛出异常并提示:

StateError: HttpUtils 默认实例未初始化!
请先调用 HttpUtils.defaultInstance(baseUrl: "your_url") 进行初始化

解决:在 main() 中添加初始化代码。


Q: 如何切换环境(开发/测试/生产)?

abstract class AppConfig {
  static const String baseUrl = String.fromEnvironment(
    'API_URL',
    defaultValue: 'https://api.example.com',
  );
}

void main() {
  HttpUtils.defaultInstance(baseUrl: AppConfig.baseUrl);
  runApp(MyApp());
}

运行时指定:

flutter run --dart-define=API_URL=https://test-api.example.com

Q: 如何动态修改 BaseUrl?

HttpUtils().setBaseUrl('https://new-api.example.com');

// 获取当前 BaseUrl
String url = HttpUtils().baseUrl;

Q: 如何处理错误?

final response = await HttpUtils().get('/users');

if (response.isSuccess) {
  // 成功处理
  print(response.data);
} else {
  // 错误处理
  print('错误码: ${response.code}');
  print('错误信息: ${response.message}');
}

错误码说明

错误码说明
-100BaseUrl 未设置
-1业务错误
-2数据解析错误
401未授权(触发自动刷新)
403无权限
404资源不存在
500服务器错误

Q: 如何取消请求?

final cancelToken = CancelToken();

// 发起请求
final future = HttpUtils().get('/data', cancelToken: cancelToken);

// 取消
cancelToken.cancel('用户取消');

// 处理
try {
  await future;
} catch (e) {
  if (e is DioException && e.type == DioExceptionType.cancel) {
    print('请求已取消');
  }
}

Q: 如何管理转换器?

// 移除转换器
HttpUtils.removeConverter<User>();

// 清空所有转换器
HttpUtils.clearConverters();

Q: 如何清理待重试的请求?

退出登录时调用:

HttpUtils.clearPendingRequests();

📝 最佳实践

Repository 模式

class UserRepository {
  Future<List<User>> getUsers() async {
    final response = await HttpUtils().get<List<User>>('/users');
    if (response.isSuccess) {
      return response.data ?? [];
    }
    throw Exception(response.message);
  }
  
  Future<void> createUser(User user) async {
    final response = await HttpUtils().post(
      '/users',
      data: user.toJson(),
    );
    if (!response.isSuccess) {
      throw Exception(response.message);
    }
  }
}

统一错误处理

Future<T?> safeRequest<T>(
  Future<ApiResponse<T>> Function() request,
) async {
  try {
    final response = await request();
    if (response.isSuccess) {
      return response.data;
    } else {
      showToast(response.message);
      return null;
    }
  } catch (e) {
    showToast('网络请求失败');
    return null;
  }
}

// 使用
final users = await safeRequest(
  () => HttpUtils().get<List<User>>('/users'),
);

📄 许可证

MIT License