☰
Flutter for OpenHarmony:用 retrofit 为鸿蒙应用打造类型安全的网络请求层(Dio 增强实践)
2026/9/29 3:40:20 网站建设 项目流程

1. 鸿蒙 Flutter 项目里,Dio 调用为什么会越写越乱

在 OpenHarmony 上做 Flutter 应用,网络层往往是第一个失控的地方。刚开始只有三五个接口,你直接在页面里Dio().get(...)也能跑;等到接口涨到三四十个,问题就集中爆发了:URL 拼错没人发现、response.data['data']['list']这种取值散落在十几个文件里、后端字段一改名就得全局搜索替换、每个页面都要重复写 try-catch 和 loading 状态。

我试过在一个鸿蒙侧边栏工具项目里手工维护 Dio 调用,最典型的一次事故是:后端把/user/profile改成了/user/info,我漏改了设置页那一处,结果首页正常、设置页白屏,排查了半小时才定位到。这类问题的根因不是 Dio 不好用,而是缺少一层类型安全的接口契约。

retrofit 解决的正是这件事。它让你用抽象类 + 注解声明接口,由retrofit_generator在编译期生成实现代码,运行时直接调用生成的方法,返回值就是强类型对象。对 OpenHarmony 来说还有一层额外好处:生成的是纯 Dart 代码,不依赖运行时反射,在鸿蒙的 AOT 编译链路下初始化更快、行为更可预测。

这篇面向的是已经在鸿蒙上跑通 Flutter 工程、想把手写 Dio 收敛成接口层的开发者。下面从依赖配置一路写到真实请求验证,配置和命令都可以直接复制。

2. 前置准备:TaoToken 接入与工程依赖

2.1 先拿到可用的模型服务入口

retrofit 只是网络层的“壳”,它需要一个真实可访问的后端才能验证。如果你手头暂时没有鸿蒙后端,可以先用 TaoToken 提供的模型服务做联调目标,它的接口是标准 REST 风格,很适合拿来跑通整条链路。

注册和取 Key 的入口在这里:官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,进入后到控制台创建 API Key,地址是 https://taotoken.net/console 。API 基址用 https://taotoken.net/api ,注意这个地址后面不要拼多余的路径,retrofit 的baseUrl会自己处理。

拿到 Key 之后先别急着写代码,用 curl 确认一下链路通不通:

curl https://taotoken.net/api/v1/models \ -H "Authorization: Bearer sk-你的Key"

返回一个包含data数组的 JSON 就说明 Key 有效。这一步很重要,因为后面 retrofit 报错时,你需要能区分是“网络层写错了”还是“Key 本身无效”。

2.2 pubspec 依赖配置

在pubspec.yaml里加上运行期依赖和生成期依赖。注意retrofit、dio、json_annotation放dependencies,生成器放dev_dependencies:

dependencies: flutter: sdk: flutter dio: ^5.4.0 retrofit: ^4.1.0 json_annotation: ^4.8.1 dev_dependencies: flutter_test: sdk: flutter retrofit_generator: ^8.1.0 json_serializable: ^6.7.1 build_runner: ^2.4.8

版本号不要照抄,用flutter pub outdated看一下当前解析结果。鸿蒙 Flutter 分支的 SDK 版本可能和主干有差异,如果retrofit_generator报 analyzer 版本冲突,优先降retrofit_generator而不是升 Dart SDK。

装依赖:

flutter pub get

2.3 build.yaml 配置

在工程根目录新建build.yaml,显式声明生成器行为。默认配置在多数项目能用,但显式写出来能避免多人协作时的隐式差异:

targets: $default: builders: retrofit_generator: options: null_safety: true json_serializable: options: explicit_to_json: true field_rename: snake

field_rename: snake表示 Dart 的userName会自动映射到 JSON 的user_name,省掉大量@JsonKey注解。如果你的后端是驼峰命名,把这行删掉即可。

3. 可复制配置:接口定义与鸿蒙权限声明

3.1 定义类型安全的 API 接口

新建lib/network/api_service.dart。核心是抽象类 +part指令,part指向的文件名必须和当前文件名对应:

import 'package:dio/dio.dart'; import 'package:retrofit/retrofit.dart'; import 'model/chat_response.dart'; part 'api_service.g.dart'; @RestApi() abstract class ApiService { factory ApiService(Dio dio, {String? baseUrl}) = _ApiService; @GET("/v1/models") Future<ModelListResponse> listModels(); @POST("/v1/chat/completions") Future<ChatResponse> createChat( @Body() Map<String, dynamic> body, ); }

这里@RestApi()不写baseUrl,改由实例化时传入,这样鸿蒙设备在 WiFi 和蜂窝网之间切换、或者测试环境和生产环境切换时,不用改代码。

3.2 定义返回模型

新建lib/network/model/chat_response.dart。字段名和 JSON 的映射交给json_serializable:

import 'package:json_annotation/json_annotation.dart'; part 'chat_response.g.dart'; @JsonSerializable() class ChatResponse { final String id; final String model; final List<Choice> choices; ChatResponse({ required this.id, required this.model, required this.choices, }); factory ChatResponse.fromJson(Map<String, dynamic> json) => _$ChatResponseFromJson(json); Map<String, dynamic> toJson() => _$ChatResponseToJson(this); } @JsonSerializable() class Choice { final int index; final Message message; Choice({required this.index, required this.message}); factory Choice.fromJson(Map<String, dynamic> json) => _$ChoiceFromJson(json); Map<String, dynamic> toJson() => _$ChoiceToJson(this); } @JsonSerializable() class Message { final String role; final String content; Message({required this.role, required this.content}); factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(json); Map<String, dynamic> toJson() => _$MessageToJson(this); }

ModelListResponse同理,按/v1/models的实际返回结构写一个即可,这里不展开。

3.3 执行代码生成

dart run build_runner build --delete-conflicting-outputs

成功后会看到api_service.g.dart、chat_response.g.dart等文件生成。如果报part文件找不到,检查两点:part指令的文件名拼写、以及抽象类是否在part之前声明。

生成完成后建议把.g.dart提交到版本库。鸿蒙 CI 环境不一定装了 build_runner,提交生成产物能让流水线更稳定。

3.4 鸿蒙端网络权限声明

这一步是鸿蒙特有的,漏了会直接导致请求失败且报错信息很模糊。在ohos/entry/src/main/module.json5的requestPermissions中加入网络权限:

{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" }, { "name": "ohos.permission.GET_NETWORK_INFO" } ] } }

INTERNET是发起请求必需,GET_NETWORK_INFO用于读取当前网络状态,配合 Dio 拦截器做断网提示时会用到。改完module.json5需要重新编译鸿蒙侧工程,热重载不会生效。

4. 验证请求:一次真实调用与成功结果

4.1 组装 Dio 与拦截器

新建lib/network/api_client.dart,把 Dio 实例、鉴权头、日志拦截器集中管理:

import 'package:dio/dio.dart'; import 'api_service.dart'; class ApiClient { static const String _baseUrl = "https://taotoken.net/api"; late final Dio _dio; late final ApiService service; ApiClient({required String apiKey}) { _dio = Dio( BaseOptions( baseUrl: _baseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 60), headers: { "Authorization": "Bearer $apiKey", "Content-Type": "application/json", }, ), ); _dio.interceptors.add( LogInterceptor(requestBody: true, responseBody: true), ); service = ApiService(_dio); } }

receiveTimeout给到 60 秒,是因为模型类接口的首 token 延迟可能较长,15 秒容易误判为超时。

4.2 发起一次真实请求

在main.dart里写一个最小验证入口:

import 'network/api_client.dart'; Future<void> main() async { final client = ApiClient(apiKey: "sk-你的Key"); try { final resp = await client.service.createChat({ "model": "gpt-4o-mini", "messages": [ {"role": "user", "content": "用一句话说明什么是类型安全"} ], }); print("id: ${resp.id}"); print("model: ${resp.model}"); print("content: ${resp.choices.first.message.content}"); } on DioException catch (e) { print("status: ${e.response?.statusCode}"); print("body: ${e.response?.data}"); } }

运行flutter run -d ohos(设备名按你本地flutter devices的输出替换)。控制台会先打印 LogInterceptor 的请求日志,然后输出模型返回内容。看到content:后面有正常文本,说明从鸿蒙设备到 TaoToken 的整条链路已经打通,retrofit 生成的实现类工作正常。

4.3 验证类型安全是否真的生效

把createChat的返回类型临时改成Future<String>,重新执行build_runner build,你会看到编译期直接报错,而不是等到运行时才崩。这就是 retrofit 相对手写 Dio 的核心价值:字段拼错、类型不匹配、方法名写错,全部在编译阶段暴露。

5. 本篇常见错排查

5.1 报错Target of URI doesn't exist: 'api_service.g.dart'

生成文件没产出。先确认dart run build_runner build是否真的执行成功,再检查part 'api_service.g.dart';的文件名是否和当前 Dart 文件同名。如果 build_runner 卡住,删掉.dart_tool/build目录重跑。

5.2 报错The method '_$ChatResponseFromJson' isn't defined

json_serializable没跑或者模型类没加@JsonSerializable()。检查模型文件顶部的part指令,以及build.yaml里json_serializable是否被误删。

5.3 鸿蒙设备上请求直接失败,日志无响应体

九成是权限问题。回到module.json5确认ohos.permission.INTERNET已声明,并且鸿蒙工程重新编译过。另一个可能是baseUrl结尾多了斜杠,导致拼出//v1/chat/completions,部分网关会拒绝这种路径。

5.4 返回 401 或 403

Key 无效或没带上。用第 2.1 节的 curl 命令单独验证 Key,排除是代码问题。注意Authorization头的Bearer后面有一个空格,漏掉空格会直接 401。

5.5 返回 200 但解析报type 'Null' is not a subtype of type 'String'

后端某个字段返回了 null,而 Dart 模型声明为非空。把对应字段改成String?并在使用处做空判断,或者用@JsonKey(defaultValue: "")给默认值。

5.6 生成代码后热重载不生效

.g.dart属于编译期产物,改动后需要完全重启应用,热重载不会重新加载。养成改完接口定义就flutter run重启的习惯。

6. 把网络层继续收敛下去

接口层跑通之后,下一步通常是把不同业务域的 API 拆成多个 Service 类,比如UserService、ChatService、FileService,各自独立生成,避免单个文件膨胀到几百行。Dio 拦截器则统一承担 Token 刷新、错误码映射、重试逻辑,Service 层只关心业务语义。

如果你打算把这条链路用到长期编码或 Agent 类项目里,可以了解一下 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,它更适合需要持续调用、批量任务的场景。想先在浏览器里直接验证模型返回格式,用模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 更快,不用每次都跑一遍 Flutter 工程。Key 的管理和轮换在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入细节和参数说明看文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

最后留一个实操建议:把build_runner的生成命令写进Makefile或鸿蒙工程的构建脚本,每次拉取代码后自动跑一次,能避免“别人改了接口定义但忘了生成”这类协作问题。网络层这种东西,越早收敛越省事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询