--- name: remote-api-development description: Use when adding, modifying, or refactoring any remote/HTTP API call in this Flutter project. Invoke for new endpoints, data sources, repositories, providers, or when removing hardcoded URLs and inline response parsing. license: MIT metadata: version: "1.0.0" domain: networking triggers: API, endpoint, remote call, DioClient, data source, repository, provider, userInfo, ApiEndpoints role: specialist scope: implementation output-format: code related-skills: flutter-expert, flutter-tester --- # Remote API Development Standards 本 skill 定义 fly-narwhal-flutter 项目远程接口调用的统一开发规范。新增或修改任何 HTTP 接口调用时必须遵循。 ## When to Use This Skill - 新增一个后端接口调用 - 重构内联在 Provider/Widget 中的硬编码 URL 或内联解析逻辑 - 新增/修改 RemoteDataSource、Repository、Provider - 排查“同一个接口存在多处重复实现”的问题 ## 分层架构(必须遵守) 请求自上而下分层,禁止跨层或在上层内联实现下层职责: ``` UI (Widget / Screen) └─ Provider (FutureProvider / Notifier) // 仅编排,不写 URL/解析 └─ Repository (可选,做实体映射 / 多源聚合) └─ RemoteDataSource // 唯一发起请求 + 解析响应的地方 └─ DioClient (core/network) // 统一网络层 └─ ApiEndpoints // 唯一的 URL 常量来源 ``` ## 核心规则 ### 规则 1:URL 必须集中在 ApiEndpoints - 所有接口路径只能定义在 `lib/core/constants/app_constants.dart` 的 `ApiEndpoints` 类中。 - 严禁在 DataSource/Provider/Widget 中出现硬编码字符串路径(如 `'/v/api/v1/user/info'`)。 - 带路径参数的接口用静态方法,如 `static String itemByGuid(String guid) => '$itemPrefix/$guid';`。 ```dart // app_constants.dart class ApiEndpoints { const ApiEndpoints._(); static const String userInfo = '/v/api/v1/user/info'; } ``` ### 规则 2:请求与解析必须封装在 RemoteDataSource - 所有 `_dioClient.get/post/...` 调用只能出现在 `lib/data/datasources/remote/` 下的 DataSource 中。 - 用 `converter` 回调解析响应,统一通过 `FnBaseResponse` 处理。 - 业务错误码判断使用 `ResponseCodes.success`(即 0),失败时 `throw Exception(baseResponse.msg)`。 - 返回类型统一为 `ApiResult`,禁止直接返回原始 `Response`。 - DataSource 依赖 **core 版** `DioClient`(`lib/core/network/dio_client.dart`),不要用 legacy 版(`lib/data/network/dio_client.dart`)。 ```dart class UserRemoteDataSource { final DioClient _dioClient; UserRemoteDataSource(this._dioClient); // Get current user info Future> getUserInfo() async { return _dioClient.get( ApiEndpoints.userInfo, converter: _parseUserInfoResponse, ); } // Parse FnBaseResponse and unwrap data UserInfo _parseUserInfoResponse(dynamic data) { final baseResponse = FnBaseResponse.fromJson( data, (json) => UserInfo.fromJson( json is Map ? json : Map.from(json as Map), ), ); if (baseResponse.code != ResponseCodes.success) { throw Exception(baseResponse.msg); } return baseResponse.data ?? UserInfo(guid: '', username: '', isAdmin: 0); } } ``` ### 规则 3:Provider 只做编排,不写请求细节 - Provider 中禁止出现 URL、`FnBaseResponse` 解析、错误码判断。 - 先定义 `xxxRemoteDataSourceProvider`,再让业务 Provider 委托它。 - 用 `ApiResult.getOrThrow()` 把失败转为异常,交给 `AsyncValue.error` 处理;对外类型保持稳定(如 `FutureProvider`)。 ```dart // DataSource provider —— 用 core 版 DioClient 构造 final userRemoteDataSourceProvider = Provider((ref) { final prefsManager = ref.watch(preferencesManagerProvider); final dioClient = core_network.DioClient.withCallbacks( getToken: () => prefsManager.getToken() ?? '', getCookie: () => prefsManager.getCookie() ?? '', getAuthCode: () => prefsManager.getAuthCode() ?? '', getBaseUrl: () => prefsManager.getBaseUrl() ?? '', ); return UserRemoteDataSource(dioClient); }); // Business provider —— 只编排,不写请求细节 final userInfoProvider = FutureProvider((ref) async { final dataSource = ref.read(userRemoteDataSourceProvider); final result = await dataSource.getUserInfo(); return result.getOrThrow(); }); ``` ### 规则 4:Repository 层(按需) - 当需要做 model→entity 映射或聚合多个数据源时,新增 Repository(`lib/data/repositories/`)实现 `lib/domain/repositories/` 接口。 - Repository 通过 `ApiResult.map` 转换数据,保持 `ApiResult` 链路。 ```dart @override Future> getUserInfo() async { final result = await _remoteDataSource.getUserInfo(); return result.map((data) => UserMapper.toEntity(data)); } ``` ## 反模式(禁止) - ❌ 在 Provider/Widget 里写 `dioClient.dio.get('/v/api/v1/...')` 硬编码 URL。 - ❌ 在 Provider/Widget 里内联 `FnBaseResponse.fromJson` 解析。 - ❌ 同一接口在多处重复实现请求与解析逻辑。 - ❌ DataSource 使用 legacy `DioClient`(`lib/data/network/dio_client.dart`)。 - ❌ 直接返回 `Response`,绕过 `ApiResult`。 ## 新增接口操作清单 1. 在 `ApiEndpoints` 添加路径常量(或路径参数方法)。 2. 在对应 `RemoteDataSource` 添加方法,用 `converter` 解析,返回 `ApiResult`。 3. 需要实体映射时在 Repository 添加方法并 `map`。 4. 在 `providers.dart` 添加/复用 `xxxRemoteDataSourceProvider`,业务 Provider 委托调用。 5. 验证:运行 `flutter analyze`,确认无 unused/undefined;涉及 UI 时按 flutter-tester 规范补充测试或 mcp UI 验证。 ## 参考实现 - URL 常量:`lib/core/constants/app_constants.dart` - DataSource:`lib/data/datasources/remote/user_remote_data_source.dart` - 网络层:`lib/core/network/dio_client.dart`、`lib/core/network/api_result.dart` - Provider:`lib/providers/providers.dart`(`userInfoProvider`、`userRemoteDataSourceProvider`)