app scaffold
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
/// 网络层。来源:conti-docs/05-networking.md。
|
||||
///
|
||||
/// 对外只暴露 [ApiClient] 和 `apiClientProvider`——**repository 一律注入
|
||||
/// ApiClient,不注入 Dio**(05)。`dioProvider` 也导出,但只给需要直接持有
|
||||
/// Dio 的极少数场景(目前没有)。
|
||||
///
|
||||
/// 依赖约束(01):`core_network → core_auth` 是允许的三条 core 互依例外之一;
|
||||
/// 其余跨包需要(日志、设备信息)走 `src/ports.dart` 的接口反转。
|
||||
library;
|
||||
|
||||
export 'src/api_client.dart';
|
||||
export 'src/paging.dart';
|
||||
export 'src/ports.dart';
|
||||
export 'src/providers.dart';
|
||||
@@ -0,0 +1,99 @@
|
||||
/// 仓库唯一对外的 HTTP 出口。来源:conti-docs/05-networking.md。
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:core_foundation/core_foundation.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// 包在 [Dio] 外面的一层。
|
||||
///
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// 为什么需要它:**拦截器没有办法让 `dio.get()` 抛出 [AppException]**。
|
||||
/// dio 的错误通道只认 [DioException],我们的 [AppException] 只能挂在它的
|
||||
/// `error` 字段上。repository 直接调 `dio.get()` 的话,业务层写
|
||||
///
|
||||
/// try { ... } on UnauthorizedException { ... }
|
||||
///
|
||||
/// 永远进不来——实际抛出来的仍然是 [DioException]。
|
||||
///
|
||||
/// 所以在出口处把 `DioException.error` 拆出来重抛。
|
||||
///
|
||||
/// **规则:repository 一律注入 [ApiClient],不注入 [Dio]。** 全仓库只有
|
||||
/// core_network 内部和 core_auth 的裸 Dio 会直接碰 [Dio] 类型。
|
||||
/// ---------------------------------------------------------------------------
|
||||
class ApiClient {
|
||||
/// [dio] 由 `dioProvider` 提供。
|
||||
ApiClient(this._dio);
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
/// GET。
|
||||
Future<T> get<T>(String path, {Map<String, dynamic>? query, CancelToken? cancelToken}) =>
|
||||
_run(() => _dio.get<T>(path, queryParameters: query, cancelToken: cancelToken));
|
||||
|
||||
/// POST。
|
||||
Future<T> post<T>(String path, {Object? data, CancelToken? cancelToken}) =>
|
||||
_run(() => _dio.post<T>(path, data: data, cancelToken: cancelToken));
|
||||
|
||||
/// PUT。
|
||||
Future<T> put<T>(String path, {Object? data, CancelToken? cancelToken}) =>
|
||||
_run(() => _dio.put<T>(path, data: data, cancelToken: cancelToken));
|
||||
|
||||
/// DELETE。
|
||||
Future<T> delete<T>(String path, {Object? data, CancelToken? cancelToken}) =>
|
||||
_run(() => _dio.delete<T>(path, data: data, cancelToken: cancelToken));
|
||||
|
||||
/// 多文件上传。
|
||||
///
|
||||
/// 约定(05 §文件与图片上传):
|
||||
/// - **上传前必须压缩**。门店员工直接拍的照片通常 3–8 MB,原图在门店 WiFi 下
|
||||
/// 大概率超时。统一压到长边 1600px / JPEG 80,超 2 MB 再降一档。
|
||||
/// - **进度必须可见**,否则用户会以为卡死反复点。
|
||||
/// - **失败要能单张重传**,所以 UI 的上传状态按单张维护,别整批重来。
|
||||
/// - [FormData] **不可重用**:它是流,重试必须重新构造,复用会报
|
||||
/// stream already listened——所以这个方法每次调用都自己建一个。
|
||||
Future<T> upload<T>(
|
||||
String path, {
|
||||
required List<File> files,
|
||||
Map<String, dynamic>? fields,
|
||||
void Function(int sent, int total)? onProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
final FormData formData = FormData.fromMap(<String, dynamic>{
|
||||
...?fields,
|
||||
'files': <MultipartFile>[
|
||||
for (final File f in files)
|
||||
await MultipartFile.fromFile(f.path, filename: p.basename(f.path)),
|
||||
],
|
||||
});
|
||||
|
||||
return _run(
|
||||
() => _dio.post<T>(
|
||||
path,
|
||||
data: formData,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onProgress,
|
||||
// 上传单独放宽:用全局的 30s 传几张原图会超。
|
||||
options: Options(sendTimeout: const Duration(minutes: 3)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<T> _run<T>(Future<Response<T>> Function() send) async {
|
||||
try {
|
||||
final Response<T> res = await send();
|
||||
return res.data as T;
|
||||
} on DioException catch (e, st) {
|
||||
final Object? error = e.error;
|
||||
// 拦截器已经归一化过,这里只负责把它从 DioException 的壳里拿出来重抛。
|
||||
// Error.throwWithStackTrace 保留原始堆栈——否则上报到崩溃平台的堆栈会
|
||||
// 全部指向下面这一行,等于没有堆栈。
|
||||
if (error is AppException) {
|
||||
Error.throwWithStackTrace(error, st);
|
||||
}
|
||||
Error.throwWithStackTrace(const NetworkException('网络异常,请稍后重试'), st);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/// 后端统一响应包装的解包。来源:conti-docs/05-networking.md §后端契约。
|
||||
library;
|
||||
|
||||
import 'package:core_foundation/core_foundation.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'ports.dart';
|
||||
|
||||
/// 把 `ApiResult<T> { code, message, data, traceId }` 剥成里层的 `data`。
|
||||
///
|
||||
/// **解包只在这里做一次**,repository 拿到的 `response.data` 已经是 `data` 本身。
|
||||
class ApiResultInterceptor extends Interceptor {
|
||||
/// [log] 见 [apiLogSinkProvider]。
|
||||
ApiResultInterceptor(this._log);
|
||||
|
||||
final ApiLogSink _log;
|
||||
|
||||
@override
|
||||
void onResponse(Response<dynamic> response, ResponseInterceptorHandler handler) {
|
||||
final Object? body = response.data;
|
||||
|
||||
// 非 JSON 对象响应(如文件下载)不走解包。
|
||||
if (body is! Map<String, dynamic> || !body.containsKey('code')) {
|
||||
handler.next(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 用 num 而不是 int:万一后端某个字段序列化成了 JSON 浮点数,as int 会直接抛。
|
||||
final int? code = (body['code'] as num?)?.toInt();
|
||||
final String? traceId = body['traceId'] as String?;
|
||||
|
||||
// traceId 必须留存:用户报一个 traceId,后端就能在日志里定位这次请求
|
||||
// (backend 06/08)。成功失败都要打。URL 不带 query——H5 那类 URL 里有 ticket。
|
||||
final Uri uri = response.requestOptions.uri;
|
||||
_log('[api] ${uri.origin}${uri.path} code=$code traceId=$traceId');
|
||||
|
||||
if (code == ApiCode.ok) {
|
||||
response.data = body['data'];
|
||||
handler.next(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// code != 0 一律转成业务异常,不让它以"成功响应"的形态流到业务层。
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
error: BusinessException(
|
||||
code ?? -1,
|
||||
(body['message'] as String?) ?? '请求失败',
|
||||
traceId: traceId,
|
||||
),
|
||||
),
|
||||
// callFollowingErrorInterceptor:让 ErrorMappingInterceptor 有机会放行它。
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/// 鉴权与 401 刷新。来源:conti-docs/05-networking.md §Token 刷新:必须串行,失败即登出。
|
||||
library;
|
||||
|
||||
import 'package:core_auth/core_auth.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'providers.dart';
|
||||
|
||||
/// 附加 token;401 时刷新一次并重放原请求。
|
||||
///
|
||||
/// 串行化本身**不在这里**——它在 core_auth 的 [TokenRefresher] 里(共享在途
|
||||
/// Future)。这样即使将来多了一个走刷新的调用方,串行保证也只有一份实现。
|
||||
///
|
||||
/// 三条硬约束(后端 refresh token 一次性 + 重放即全量撤销):
|
||||
/// 1. 绝不并发刷新,否则用户被全设备强制登出;
|
||||
/// 2. 刷新失败不重试,直接登出;
|
||||
/// 3. 刷新请求本身走裸 Dio,不经过本拦截器,否则无限递归。
|
||||
class AuthInterceptor extends Interceptor {
|
||||
/// [ref] 用来读 token 与会话。
|
||||
AuthInterceptor(this._ref);
|
||||
|
||||
final Ref _ref;
|
||||
|
||||
/// 一次性重试标记。带着新 token 重放后又 401,说明不是 token 的问题,别再刷了。
|
||||
static const String _retriedKey = 'x-retried';
|
||||
|
||||
@override
|
||||
Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
final String? token = await _ref.read(tokenStorageProvider).readAccessToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
if (err.response?.statusCode != 401) {
|
||||
handler.next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.requestOptions.extra[_retriedKey] == true) {
|
||||
await _logout();
|
||||
handler.next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
final TokenPair refreshed;
|
||||
try {
|
||||
refreshed = await _ref.read(tokenRefresherProvider).refresh();
|
||||
} on Object {
|
||||
// 刷新失败 = refresh token 已失效。不重试——再试一次只会再触发一次
|
||||
// 重放判定,把用户的其他设备也一起踢掉。
|
||||
await _logout();
|
||||
handler.next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final RequestOptions options = err.requestOptions
|
||||
..extra[_retriedKey] = true
|
||||
..headers['Authorization'] = 'Bearer ${refreshed.accessToken}';
|
||||
handler.resolve(await _ref.read(dioProvider).fetch<dynamic>(options));
|
||||
} on DioException catch (e) {
|
||||
handler.next(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logout() =>
|
||||
_ref.read(sessionProvider.notifier).logout(reason: LogoutReason.tokenExpired);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/// 异常归一化。来源:conti-docs/05-networking.md §异常归一化。
|
||||
library;
|
||||
|
||||
import 'package:core_foundation/core_foundation.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
/// 把 [DioException] 统一转成 [AppException] 体系。
|
||||
///
|
||||
/// **必须排在拦截器链的最后**:它把所有还没被归一化的错误兜底成 [NetworkException],
|
||||
/// 排在前面会把 [ApiResultInterceptor] 抛的 [BusinessException] 提前吃掉。
|
||||
class ErrorMappingInterceptor extends Interceptor {
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
// 已经是 AppException 的直接放行,不要二次包装。
|
||||
if (err.error is AppException) {
|
||||
handler.next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
final AppException mapped = switch (err.type) {
|
||||
DioExceptionType.connectionTimeout => const NetworkException(
|
||||
'网络超时,请检查网络后重试',
|
||||
kind: NetworkErrorKind.connectTimeout,
|
||||
),
|
||||
DioExceptionType.sendTimeout => const NetworkException(
|
||||
'网络超时,请检查网络后重试',
|
||||
kind: NetworkErrorKind.sendTimeout,
|
||||
),
|
||||
DioExceptionType.receiveTimeout => const NetworkException(
|
||||
'网络超时,请检查网络后重试',
|
||||
kind: NetworkErrorKind.receiveTimeout,
|
||||
),
|
||||
DioExceptionType.connectionError => const NetworkException(
|
||||
'网络不可用,请检查网络后重试',
|
||||
kind: NetworkErrorKind.noConnection,
|
||||
),
|
||||
// 必须静默处理:用户返回上一页时在途请求被取消,弹提示是纯粹的噪音(12)。
|
||||
DioExceptionType.cancel => const RequestCancelledException(),
|
||||
// 401 走到这里说明 AuthInterceptor 已经刷新失败并登出了,UI 不再重复提示。
|
||||
DioExceptionType.badResponse when err.response?.statusCode == 401 =>
|
||||
const UnauthorizedException(),
|
||||
DioExceptionType.badResponse => ServerException(
|
||||
'服务异常(${err.response?.statusCode})',
|
||||
statusCode: err.response?.statusCode,
|
||||
),
|
||||
_ => const NetworkException('网络异常,请稍后重试'),
|
||||
};
|
||||
|
||||
handler.next(
|
||||
DioException(
|
||||
requestOptions: err.requestOptions,
|
||||
response: err.response,
|
||||
type: err.type,
|
||||
error: mapped,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/// 统一请求头。来源:conti-docs/05-networking.md §统一请求头。
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:core_auth/core_auth.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'ports.dart';
|
||||
|
||||
/// 给每个请求补上追踪、版本、平台、设备和门店头。
|
||||
class HeaderInterceptor extends Interceptor {
|
||||
/// [ref] 用来读环境和会话。
|
||||
HeaderInterceptor(this._ref);
|
||||
|
||||
final Ref _ref;
|
||||
static const Uuid _uuid = Uuid();
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
final ClientInfo info = _ref.read(clientInfoProvider);
|
||||
options.headers.addAll(<String, String>{
|
||||
// 客户端生成,便于端到端串联。
|
||||
// **待与后端确认**:backend 06 说 traceId 由后端入口 filter 生成,约定是
|
||||
// 后端优先复用这个头、没有才自己生成,否则两边日志各用一套 ID 对不上。
|
||||
'X-Trace-Id': _uuid.v4(),
|
||||
'X-App-Version': info.appVersion,
|
||||
'X-Platform': Platform.isIOS ? 'ios' : 'android',
|
||||
'X-Device-Id': info.deviceId,
|
||||
});
|
||||
|
||||
// 用可空版:登录、拉门店列表这些请求本身就发生在选店之前,
|
||||
// 用会 throw 的 currentStoreIdProvider 会直接把登录流程打死。
|
||||
final int? storeId = _ref.read(currentStoreIdOrNullProvider);
|
||||
if (storeId != null) {
|
||||
// 冗余信息:access token 的 claims 里已经有 storeId,后端以 token 为准。
|
||||
// 带这个头只为排查时能一眼看出客户端当时认为自己在哪个门店——两者不一致
|
||||
// 就说明切店后 token 没换,是个 bug 信号。
|
||||
options.headers['X-Store-Id'] = '$storeId';
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/// 分页契约。来源:conti-docs/02-layering.md。
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 分页请求参数。
|
||||
@immutable
|
||||
class PageQuery {
|
||||
/// [page] 从 1 开始。
|
||||
const PageQuery({required this.page, this.size = 20});
|
||||
|
||||
/// 页码,从 1 开始。
|
||||
final int page;
|
||||
|
||||
/// 每页条数。
|
||||
final int size;
|
||||
|
||||
/// 转成 query 参数。**字段名待与后端对齐**(backend 06 的分页约定还没定)。
|
||||
Map<String, dynamic> toQuery() => <String, dynamic>{'page': page, 'size': size};
|
||||
}
|
||||
|
||||
/// 分页结果。
|
||||
@immutable
|
||||
class PageResult<T> {
|
||||
/// 构造。
|
||||
const PageResult({
|
||||
required this.items,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.hasMore,
|
||||
});
|
||||
|
||||
/// 从 `{items, total, page, hasMore}` 结构解析。
|
||||
factory PageResult.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Map<String, dynamic>) itemFromJson,
|
||||
) {
|
||||
final List<dynamic> raw = (json['items'] as List<dynamic>?) ?? const <dynamic>[];
|
||||
return PageResult<T>(
|
||||
items: raw.map((dynamic e) => itemFromJson(e as Map<String, dynamic>)).toList(),
|
||||
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||
page: (json['page'] as num?)?.toInt() ?? 1,
|
||||
hasMore: json['hasMore'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 当前页数据。
|
||||
final List<T> items;
|
||||
|
||||
/// 总条数。
|
||||
final int total;
|
||||
|
||||
/// 当前页码。
|
||||
final int page;
|
||||
|
||||
/// 是否还有下一页。
|
||||
///
|
||||
/// **用后端下发的字段,不在客户端算**。02 里那份
|
||||
/// `items.length + (page - 1) * items.length < total` 的推算在最后一页
|
||||
/// 条数不满时会算错;README 的已解决项里后端已确认下发这个字段。
|
||||
final bool hasMore;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/// core_network 的对外端口。
|
||||
///
|
||||
/// 和 core_auth 的 `session_ports.dart` 是同一套思路:05 的伪代码里
|
||||
/// `HeaderInterceptor` 读了 `deviceIdProvider`、`ApiResultInterceptor` 收了一个
|
||||
/// `AppLogger`,但 01 只允许 `core_network → core_auth` 这一条 core 出边,
|
||||
/// core_logging 不在其中。所以这里只声明"需要什么",由 app 层接上去。
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// 请求头里那几个和环境无关、只有运行时才知道的值。
|
||||
@immutable
|
||||
class ClientInfo {
|
||||
/// 构造。
|
||||
const ClientInfo({required this.appVersion, required this.deviceId});
|
||||
|
||||
/// 形如 `1.4.0+142`。
|
||||
final String appVersion;
|
||||
|
||||
/// **安装级匿名 ID**:首次安装时生成的随机 UUID,存本地。
|
||||
///
|
||||
/// 绝不是 IMEI / IDFA / MAC / AndroidID——采集这些是合规红线(05 / 07 隐私清单)。
|
||||
final String deviceId;
|
||||
}
|
||||
|
||||
/// [ClientInfo] 的注入点。必须在 bootstrap 里 override。
|
||||
///
|
||||
/// 不给默认值:带着 `appVersion: 'unknown'` 上线,问题会表现为线上日志里
|
||||
/// 版本分布全糊成一团,等发现时已经排查了很久。
|
||||
final Provider<ClientInfo> clientInfoProvider = Provider<ClientInfo>(
|
||||
(Ref ref) => throw UnimplementedError('clientInfoProvider 必须在 bootstrap() 里 override'),
|
||||
);
|
||||
|
||||
/// API 日志出口。
|
||||
typedef ApiLogSink = void Function(String message);
|
||||
|
||||
/// [ApiLogSink] 的注入点。默认丢弃——测试里不用管。
|
||||
///
|
||||
/// app 层把它接到 core_logging 的 `AppLogger.d` 上。**接的时候注意 message 已经
|
||||
/// 是脱敏过的**:这里输出的只有 URL(不含 query)、code、traceId,不含请求体,
|
||||
/// 也绝不含 `Authorization`(13 §脱敏)。
|
||||
final Provider<ApiLogSink> apiLogSinkProvider = Provider<ApiLogSink>(
|
||||
(Ref ref) => (String message) {},
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
/// 拦截器链的组装。来源:conti-docs/05-networking.md §附录·拦截器链的组装。
|
||||
library;
|
||||
|
||||
import 'package:core_foundation/core_foundation.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'api_client.dart';
|
||||
import 'api_result_interceptor.dart';
|
||||
import 'auth_interceptor.dart';
|
||||
import 'error_mapping_interceptor.dart';
|
||||
import 'header_interceptor.dart';
|
||||
import 'ports.dart';
|
||||
|
||||
/// 全 App 唯一的 [Dio] 实例。
|
||||
///
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// **拦截器顺序不能改。** dio 的三个时机(onRequest / onResponse / onError)
|
||||
/// 都是按注册顺序**正向**执行的,不是洋葱模型——这一点和很多人的直觉不同。
|
||||
///
|
||||
/// HeaderInterceptor 补 trace / 版本 / 平台 / 门店头
|
||||
/// LogInterceptor 仅 enableLog;prod 绝不能开(13)
|
||||
/// AuthInterceptor 必须在 ErrorMapping 之前,才能在 401 被归一化成
|
||||
/// UnauthorizedException **之前**先尝试刷新
|
||||
/// ApiResultInterceptor 必须在 ErrorMapping 之前,它抛的 BusinessException
|
||||
/// 需要能被后者识别并放行
|
||||
/// ErrorMappingInterceptor 兜底,必须最后
|
||||
///
|
||||
/// 顺序取自 05 的附录(正文那份和附录不一致,以附录为准,见 SCAFFOLD-NOTES)。
|
||||
/// ---------------------------------------------------------------------------
|
||||
final Provider<Dio> dioProvider = Provider<Dio>((Ref ref) {
|
||||
final AppEnv env = ref.watch(appEnvProvider);
|
||||
|
||||
final Dio dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: env.apiBaseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
// 上传单独放宽到 3 分钟,见 ApiClient.upload。
|
||||
sendTimeout: const Duration(seconds: 30),
|
||||
),
|
||||
);
|
||||
|
||||
dio.interceptors.addAll(<Interceptor>[
|
||||
HeaderInterceptor(ref),
|
||||
// responseBody: false —— 响应体里可能有手机号、地址这类个人信息,
|
||||
// 而且日志只在 dev/uat 开。requestHeader 里的 Authorization 由
|
||||
// LogInterceptor 原样打印,所以 prod 必须关掉整条(env.enableLog == false)。
|
||||
if (env.enableLog) LogInterceptor(responseBody: false),
|
||||
AuthInterceptor(ref),
|
||||
ApiResultInterceptor(ref.watch(apiLogSinkProvider)),
|
||||
ErrorMappingInterceptor(),
|
||||
]);
|
||||
|
||||
ref.onDispose(dio.close);
|
||||
return dio;
|
||||
});
|
||||
|
||||
/// repository 唯一该注入的东西。
|
||||
final Provider<ApiClient> apiClientProvider = Provider<ApiClient>(
|
||||
(Ref ref) => ApiClient(ref.watch(dioProvider)),
|
||||
);
|
||||
Reference in New Issue
Block a user