Pub License: MIT

smart_dio

基于 Dio 的插件化 HTTP 客户端,专为 Flutter 设计。

核心特性

  • 插件化架构 - 全局插件 + 单次请求热插热拔,灵活组合日志/缓存/重试等能力
  • 类型安全错误 - Dart 3 sealed class,8 种错误类型,exhaustive 模式匹配
  • 多语言本地化 - 可配置错误消息,内置默认中文,支持部分覆盖
  • 多客户端管理 - 支持多后端、灰度环境,统一管理生命周期

环境要求

  • Dart SDK: >=3.0.0 <4.0.0
  • Flutter: >=3.10.0

安装

dependencies:
  smart_dio: ^<latest>

目录


快速开始

import 'package:smart_dio/smart_dio.dart';

void main() async {
  // 1. 初始化
  SmartDio.init(SmartConfig(
    options: BaseOptions(baseUrl: 'https://api.example.com'),
    globalPlugins: [
      LoggerPlugin(),
      RetryPlugin(retries: 3),
    ],
  ));

  // 2. 发起请求
  try {
    final response = await SmartDio.get<Map>('/users');
    print(response.data);
  } on SmartError catch (e) {
    // 3. 类型安全的错误处理
    switch (e) {
      case NetworkError():
        print('网络不可用');
      case TimeoutError():
        print('请求超时');
      case ResponseError(:final statusCode):
        print('服务器错误: $statusCode');
      default:
        print(e.message);
    }
  }
}

核心概念

概念 说明
SmartConfig 全局配置,包含 BaseOptions 和 globalPlugins
SmartPlugin 插件基类,定义 onStart/onRequest/onResponse/onError/onFinish 生命周期
SmartError sealed 错误基类,8 种子类型,支持 exhaustive 模式匹配
SmartClientManager 多客户端管理器,支持注册/获取/释放命名客户端
ResponseKeyMap 响应 key 映射配置,支持自定义 code/msg/data 字段名

与直接使用 Dio 的对比

能力 原生 Dio smart_dio
插件组合 手动管理 Interceptor 列表 globalPlugins + extraPlugins 声明式配置
错误处理 DioExceptionType 枚举,易遗漏分支 sealed class + exhaustive 匹配,编译期检查
重试/缓存/日志 需分别配置拦截器 内置插件,开箱即用
多语言错误消息 无内置支持 内置默认中文 + 可配置覆盖
多客户端 手动管理多个 Dio 实例 SmartClientManager 统一管理

配置与插件

SmartConfig

SmartDio.init(SmartConfig(
  // Dio 基础配置
  options: BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: Duration(seconds: 10),
    receiveTimeout: Duration(seconds: 30),
    headers: {'Accept': 'application/json'},
  ),
  // 全局插件列表
  globalPlugins: [
    LoggerPlugin(),
    HeaderPlugin({'Authorization': () => 'Bearer $token'}),
    ParamsPlugin({'platform': () => 'ios'}),
    CachePlugin.memory(),
    RetryPlugin(retries: 3),
  ],
  // 错误本地化器(可选)
  errorLocalizer: DefaultErrorLocalizer(),
));

热插热拔

// 单次请求添加插件(热插)
await SmartDio.get('/api',
  extraPlugins: [CustomPlugin()],
);

// 单次请求禁用插件(热拔)
await SmartDio.get('/realtime',
  excludePlugins: {CachePlugin},
);

// 热插 + 热拔
await SmartDio.post('/sensitive',
  extraPlugins: [EncryptPlugin()],
  excludePlugins: {LoggerPlugin},
);

插件优先级

插件按 priority 数值从小到大执行:

插件 Priority 说明
LoggerPlugin 10 最先执行,记录原始请求
HeaderPlugin 50 注入固定头部
ParamsPlugin 50 注入固定参数
CachePlugin 60 缓存处理
RetryPlugin 80 重试逻辑

内置插件

LoggerPlugin

LoggerPlugin(
  // 自定义 Talker 实例(可选)
  talker: Talker(),
)

HeaderPlugin

// 静态头部
HeaderPlugin({
  'X-App-Version': '1.0.0',
})

// 动态头部(每次请求时计算)
HeaderPlugin({
  'Authorization': () => 'Bearer ${getToken()}',
  'X-Timestamp': () => DateTime.now().millisecondsSinceEpoch.toString(),
})

ParamsPlugin

// GET 请求:添加到 queryParameters
// POST 请求:添加到 body data
ParamsPlugin({
  'platform': () => Platform.isIOS ? 'ios' : 'android',
  'version': () => packageInfo.version,
})

CachePlugin

// 内存缓存
CachePlugin.memory()

// 自定义缓存配置
CachePlugin(
  options: CacheOptions(
    store: MemCacheStore(),
    policy: CachePolicy.request,
    maxStale: Duration(days: 7),
  ),
)

RetryPlugin

// 基本用法
RetryPlugin(retries: 3)

// 自定义重试逻辑(基于 SmartError)
RetryPlugin(
  retries: 3,
  shouldRetry: (error) => switch (error) {
    NetworkError() => true,
    TimeoutError() => true,
    ResponseError(:final statusCode) => statusCode >= 500,
    _ => false,
  },
)

// 使用默认重试逻辑
RetryPlugin(shouldRetry: RetryPlugin.defaultShouldRetry)

ResponseParserPlugin

自动解析后端统一响应格式(code/msg/data),业务失败时抛出 BusinessError

// 全局配置(默认 key: code/msg/data)
ResponseParserPlugin(
  successCodes: [0, '0'],  // 成功码列表
)

// 自定义 key 映射
ResponseParserPlugin(
  keyMap: ResponseKeyMap(
    codeKey: 'status',    // 默认 'code'
    msgKey: 'message',    // 默认 'msg'
    dataKey: 'result',    // 默认 'data'
  ),
  successCodes: [200, 'success'],
)

// 自定义成功判断(优先于 successCodes)
ResponseParserPlugin(
  successChecker: (code) => code == 0 || code == 'OK',
)

单次请求覆盖 key 映射:

final response = await SmartDio.get(
  '/api/legacy',
  extra: {
    kResponseKeyMapKey: ResponseKeyMap(codeKey: 'errCode'),
  },
);

工作原理:

  1. 解析响应中的 codemsgdata 字段
  2. 根据 successCodessuccessChecker 判断业务是否成功
  3. 成功:将 data 字段作为 SmartResponse.data 返回
  4. 失败:抛出 BusinessError,包含 codeserverMessagerawResponse

错误处理

SmartError 类型

类型 说明 关键字段
NetworkError 网络不可达 host, errorCode
TimeoutError 超时(连接/发送/接收) phase, timeout
ResponseError HTTP 错误响应 statusCode, statusMessage, responseData
ParseError 数据解析失败 source, expectedType, rawSnippet
BusinessError 业务逻辑错误 code, serverMessage, traceId
CancelError 请求被取消 reason, cancelToken
CertificateError SSL 证书错误 host
UnknownError 未知错误 originalError

模式匹配

try {
  final response = await SmartDio.get('/api');
} on SmartError catch (e) {
  final message = switch (e) {
    NetworkError() => '请检查网络连接',
    TimeoutError(:final phase) => switch (phase) {
      TimeoutPhase.connect => '连接超时',
      TimeoutPhase.send => '发送超时',
      TimeoutPhase.receive => '接收超时',
    },
    ResponseError(:final statusCode) when statusCode == 401 => '请重新登录',
    ResponseError(:final statusCode) when statusCode == 404 => '资源不存在',
    ResponseError(:final statusCode) => '服务器错误 ($statusCode)',
    BusinessError(:final code, :final serverMessage) => '[$code] $serverMessage',
    CancelError() => '请求已取消',
    ParseError() => '数据解析失败',
    CertificateError() => '证书验证失败',
    UnknownError(:final originalError) => '未知错误: $originalError',
  };
  showToast(message);
}

从任意异常转换

try {
  // 可能抛出任何异常的代码
} catch (e, stackTrace) {
  final error = SmartError.from(e, stackTrace);
  // error 是 SmartError 的某个子类型
}

多语言本地化

默认中文消息

// 使用默认本地化器
SmartDio.init(SmartConfig(
  errorLocalizer: DefaultErrorLocalizer(),
));

// 获取本地化消息
final message = config.errorLocalizer.localize(error);

自定义消息

// 使用 ConfigurableErrorLocalizer 部分覆盖
final localizer = ConfigurableErrorLocalizer(
  overrides: {
    // 覆盖 NetworkError 的消息
    ErrorKey.network: (_, __) => 'Please check your network connection',

    // 使用模板
    ErrorKey.response: template('Server error: {statusCode}'),

    // 覆盖特定超时类型
    ErrorKey.timeout(TimeoutPhase.connect): (_, __) => 'Connection timed out',
  },
);

三层回退机制

1. overrides 中的覆盖配置
        ↓ (未找到)
2. fallback 本地化器
        ↓ (未设置)
3. 默认中文消息

Flutter intl 集成

class IntlErrorLocalizer implements SmartErrorLocalizer {
  final BuildContext context;
  IntlErrorLocalizer(this.context);

  @override
  String localize(SmartError error) => switch (error) {
    NetworkError() => S.of(context).error_network,
    TimeoutError() => S.of(context).error_timeout,
    // ...
  };
}

// 使用
SmartDio.init(SmartConfig(
  errorLocalizer: ConfigurableErrorLocalizer(
    fallback: IntlErrorLocalizer(context),
  ),
));

多客户端管理

注册多个客户端

// 注册主 API
SmartDio.init(SmartConfig(
  options: BaseOptions(baseUrl: 'https://api.main.com'),
));

// 注册其他 API
SmartDio.register('payment', SmartConfig(
  options: BaseOptions(baseUrl: 'https://api.payment.com'),
  globalPlugins: [LoggerPlugin(), EncryptPlugin()],
));

SmartDio.register('analytics', SmartConfig(
  options: BaseOptions(baseUrl: 'https://api.analytics.com'),
));

使用指定客户端

// 使用默认客户端
await SmartDio.get('/users');

// 使用命名客户端
await SmartDio.of('payment').doGet('/transactions');
await SmartDio.of('analytics').doPost('/events', data: eventData);

切换默认客户端

// 切换到灰度环境
SmartDio.setDefault('beta');

// 之后的请求使用 beta 客户端
await SmartDio.get('/api');

释放资源

// 释放指定客户端
SmartClientManager.instance.dispose('payment');

// 释放所有客户端
SmartClientManager.instance.disposeAll();

高级用法

自定义插件

class AuthPlugin extends SmartPlugin {
  @override
  int get priority => 40; // 在 HeaderPlugin 之前

  @override
  String get name => 'AuthPlugin';

  @override
  Future<void> onRequest(RequestContext ctx, RequestOptions options) async {
    // 注入认证头
    final token = await TokenManager.getToken();
    options.headers['Authorization'] = 'Bearer $token';
  }

  @override
  Future<void> onError(RequestContext ctx, SmartError error) async {
    // 处理 401 错误
    if (error is ResponseError && error.statusCode == 401) {
      await TokenManager.refreshToken();
      // 可以通过抛出特定异常触发重试
    }
  }
}

生命周期 Hooks

await SmartDio.get('/api',
  onStart: (ctx) async {
    print('请求开始: ${ctx.request.path}');
  },
  onSuccess: (ctx, response) async {
    print('请求成功: ${response.statusCode}');
  },
  onError: (ctx, error) async {
    print('请求失败: $error');
    // 上报错误
  },
  onFinish: (ctx) async {
    print('请求结束');
  },
);

与原生 Dio 互操作

// 获取底层 Dio 实例
final dio = SmartClientManager.instance.getDio('default');

// 添加自定义 Interceptor
dio.interceptors.add(MyCustomInterceptor());

// 直接使用 Dio
final response = await dio.get('/raw-api');

常见问题

Q: 如何处理超时?

// 全局超时配置
SmartDio.init(SmartConfig(
  options: BaseOptions(
    connectTimeout: Duration(seconds: 10),
    receiveTimeout: Duration(seconds: 30),
    sendTimeout: Duration(seconds: 30),
  ),
));

// 单次请求超时
await SmartDio.get('/api',
  optionsOverride: BaseOptions(
    receiveTimeout: Duration(minutes: 5),
  ),
);

Q: 如何取消请求?

final cancelToken = CancelToken();

// 发起请求
SmartDio.get('/api', cancelToken: cancelToken);

// 取消请求
cancelToken.cancel('User navigated away');

Q: 如何处理证书校验?

// 通过 Dio 配置
final dio = SmartClientManager.instance.getDio('default');
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
  final client = HttpClient();
  client.badCertificateCallback = (cert, host, port) => true; // 仅开发环境
  return client;
};

Q: 插件执行顺序是怎样的?

onStart (priority 升序)
    ↓
onRequest (priority 升序)
    ↓
[发起 HTTP 请求]
    ↓
onResponse (priority 降序) 或 onError (priority 降序)
    ↓
onFinish (priority 降序,始终执行)

许可证

本项目采用 MIT License

致谢

smart_dio 基于以下优秀开源项目构建(均采用 MIT License):