先说结论:一个能顺手用一年多的工具类App,其实不用等团队排期,自己花两天就能用Flutter在OpenHarmony上跑起来。这篇文章就记录这次JSON格式化工具从立项到适配的完整过程,重点讲清楚为什么用Flutter、解析逻辑怎么做、以及OpenHarmony上那些文档里不会写的坑。经常调试OpenHarmony应用的人都懂,接口返回的JSON动不动就挤成一行,缩在DevEco Studio里想找个字段全靠眼力,更别说校验、压缩这些额外操作了,每次都要把内容丢到在线工具里来回折腾。于是我干脆用Flutter自己写了一个软件开发助手App,把JSON格式化、压缩、校验、错误定位、剪贴板操作全塞进一个应用里,顺便把OpenHarmony的工程适配也一起踩平了。
这篇文章适合两类人看:一类是刚接触OpenHarmony应用开发、想找个练手项目的Flutter开发者,另一类是已经在做鸿蒙生态应用、被JSON调试逼疯的“工具党”。看完之后,你不光能复刻这个项目,还能避开我在版本坑和平台适配里交过的学费。
1. 项目思路与技术选型
1.1 开发者的真实痛点
做OpenHarmony应用调试时,我遇到最多的问题其实是“看JSON”。后端接口、消息推送、配置文件调参,全都离不开JSON结构。但OpenHarmony系统自带的开发者工具链里,没有顺手的内置JSON格式化面板。我最初的做法是把返回数据复制到电脑端的在线格式化网站,来回切换窗口,效率低到怀疑人生。更麻烦的是,真机调试时只能拿着设备操作,总不能每次都开电脑。
这个场景听起来简单,但细拆一下需求其实不少:要能格式化带缩进的JSON,要能压缩成单行传输,要能校验内容是否合法,还要在出错时告诉我到底错在哪一行。最好是打开App就能直接粘贴、一键产出结果,复制走人。这些需求如果都靠手工折腾,一天能浪费个把小时。所以做一款带在手机上的“软件开发助手”,本身就是刚需。
1.2 为什么选Flutter而不是原生ArkTS
当时摆在面前的有三条路:纯ArkTS写原生应用、用Flutter的OpenHarmony分支跨端运行、或者干脆只做Web版。我最终选Flutter,原因很实在。
第一,团队的技术栈已经沉淀在Flutter上。多端复用的价值在工具类应用里体现得很直接:同一个代码库,既能跑在OpenHarmony设备上,又能跑回Android和iOS,后面就算换平台也不需要重写逻辑。第二,Dart自带的dart:convert库对JSON的处理非常顺手,解析、编码都是一行调用,根本不用像ArkTS那样再引入额外的解析器或处理一堆类型映射。第三,UI层用Material组件搭一个双栏工具界面非常快,开发效率远高于从零写自定义组件。
当然,人在OpenHarmony上走Flutter路线也有代价。OpenHarmony官方的Flutter分支不是每个版本都跟进上游,版本相对滞后,遇到渲染引擎、平台通道上的怪问题只能自己扛。但就JSON格式化这个项目而言,它不依赖GPU渲染和复杂的原生能力,Flutter的跨端优势被放到了最大,而劣势几乎感知不到。如果做的是强交互、高频系统调用类应用,我会建议优先考虑ArkTS原生,但工具类App用Flutter,性价比很高。
1.3 功能清单与边界划分
动手之前,我给这个App划了明确的功能边界,避免做着做着就膨胀成一个“万能工具箱”。
| 功能模块 | 具体能力 | 典型使用场景 |
|---|---|---|
| JSON格式化 | 自动缩进、键值分行 | 查看接口返回、阅读配置文件 |
| JSON压缩 | 去除所有空白字符,输出单行 | 复制给后端、放入日志上报 |
| JSON合法性校验 | 解析并给出错误行、列、原因 | 排查手写JSON、脚本生成异常 |
| 剪贴板联动 | 粘贴输入、一键复制输出 | 真机上快速搬运数据 |
| 文本统计 | 字符数、行数、耗时 | 了解数据规模、评估解析性能 |
| 大文本处理 | 后台isolate处理,避免卡UI | 处理MB级的大JSON文件 |
边界在哪里呢?不做在线请求、不做历史记录云同步、不做语法高亮编辑器。这些功能要么牵扯云端账号,要么复杂度指数级上升,对一个“轻量助手”来说全是负担。先把格式化、压缩、校验这三个核心做好,工具才称得上工具。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与工程搭建
2.1 OpenHarmony对应Flutter SDK的版本匹配
OpenHarmony上跑Flutter,最忌讳的就是“拿最新的Flutter SDK直接开干”。官方维护的flutter_flutter仓库有一堆专门适配OpenHarmony的分支,比如OpenHarmony_Flutter_3.7.22这类带版本号的命名。你必须先确认好自己准备用哪个OpenHarmony版本,再决定拉取哪个分支,顺序反了就会在编译期看到一堆莫名其妙的问题。
我当时的环境大致是这样准备的:
bash复制git clone https://gitee.com/openharmony/flutter_flutter.git -b OpenHarmony_Flutter_3.7.22
export PATH=$PATH:/path/to/flutter_flutter/bin
flutter doctor
flutter doctor在这里不是走个过场,它会明确提示OpenHarmony相关工具链是否就位,包括DevEco Studio、ohpm、SDK路径等。如果之前机器上装过原版Flutter,务必把PATH指到新克隆的OpenHarmony分支上,否则后患无穷。这个步骤值得多花十分钟,后面少走一星期弯路。
2.2 创建工程与目录结构
SDK就绪后,创建工程比想象中顺利:
bash复制flutter create --project-name json_tools --platforms ohos,android,ios json_tools_app
cd json_tools_app
flutter pub get
--platforms ok之后,项目里会自动生成ohos目录,这就是OpenHarmony侧的原生工程壳子,后续由DevEco Studio打开编译成hap包。目录结构大致是:
code复制lib/
main.dart
pages/home_page.dart
logic/json_tool.dart
ohos/
entry/
src/main/module.json5
src/main/ets/
src/main/cpp/
这里要特别提醒:ohos目录下的entry里也有一套ets代码,但正常情况下不用大改,它是用来承接Flutter引擎初始化的。如果哪天看到module.json5里的pages列表为空,别慌,那不代表应用没页面,Flutter页面是独立渲染的,原生壳只负责提供引擎容器。
2.3 构建hap与真机安装
编译OpenHarmony应用产物和Android不一样,命令上就有区别:
bash复制flutter build hap --debug
构建完成后,build/ohos/outputs里会生成.hap安装包。通过hdc工具或者DevEco Studio的设备管理器都能把包安装到开发板上。我第一次构建时卡在“找不到SDK配置”上,排查后发现是local.properties里的SDK路径写错了。如果报这类错误,先去检查工程根目录的local.properties,确认里面指向的是OpenHarmony SDK的安装位置,而不是Android SDK。
还有一点,OpenHarmony的Flutter分支对构建工具链的版本很敏感。出现“The current configured Flutter SDK is not known to be fully supported”这类提示时,往往不是大问题,就是当前SDK版本超出了插件支持范围,根据提示把Flutter切换到对应分支,或者按pubspec.lock锁定插件版本即可。
3. 核心功能实现与解析原理
3.1 JSON解析层:decode和encode的角色分工
JSON工具的心脏是dart:convert库。大家都知道json.decode把字符串变成Dart对象,json.encode把对象变回字符串,但真正做工具时还得想清楚边界。
先看核心解析函数:
dart复制import 'dart:convert';
Object? decodeFlexible(String input) {
final trimmed = input.trim();
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
throw const FormatException('输入内容不是JSON对象或数组');
}
return json.decode(trimmed);
}
为什么要加一个startsWith限制?因为从严格意义来说,"abc"、123、true也都是合法JSON。但对一个格式化工具来说,单独格式化一个字符串常量没有意义,而且用户在绝大多数场景下贴进来的都是对象或数组。提前拦截无效输入,比解析后再生硬地判断类型要友好得多。
json.decode解析完的对象会变成Map<String, dynamic>、List<dynamic>、num、String、bool的组合。这里有一个隐藏坑:Dart的num在解码整数和浮点数时自适应,大整数超出一定精度会被转成浮点数,导致长ID、大数字失真。如果你要处理的JSON里经常有超过JavaScript安全整数范围的数字,那这个工具只适合做“查看结构”,不适合做“保真转换”。真要保真,就得引入支持任意精度的解析器,复杂度直接上一个台阶。
3.2 格式化、压缩与校验三合一
格式化输出的实现清爽到让人怀疑:
dart复制String formatJson(String input, {int indent = 2}) {
final obj = decodeFlexible(input);
return const JsonEncoder.withIndent(' ').convert(obj);
}
String minifyJson(String input) {
final obj = decodeFlexible(input);
return json.encode(obj);
}
JsonEncoder.withIndent(' ')会按照传入的缩进符,把Map和List一层层排出来,对象键按编码顺序输出,每行一个键值对。而json.encode则完全不保留空格,输出单行紧凑文本。这两个函数加在一起,格式化与压缩就都有了。
逻辑似乎简单,但校验才是真正体现功底的地方。Dart的json.decode失败时会抛出FormatException,异常里有message和offset两个关键字段,前者是错误描述,后者是错误在原始字符串中的偏移位置。用户拿到offset并没有用,他们需要的是“第几行第几列”,这个换算要自己来:
dart复制int calcLine(String text, int offset) {
return text.substring(0, offset).split('\n').length;
}
int calcColumn(String text, int offset) {
final lastLineStart = text.lastIndexOf('\n', offset - 1);
return offset - lastLineStart;
}
String parseErrorInfo(String input, FormatException e) {
final line = calcLine(input, e.offset);
final column = calcColumn(input, e.offset);
return '第 $line 行,第 $column 列附近:${e.message}';
}
这段代码的关键在于lastIndexOf('\n', offset - 1),它能精确找到offset所在行的起始位置。我踩过一次坑,最初直接用line去切行号,结果多行JSON里换行符计数总是差一位。后来换成“先算行、再算列”的方式,错误提示才真正可读。
3.3 大JSON文件运算的isolate优化
格式化一个几千字节的JSON,主线程瞬间就能完成,用户根本感觉不到。但总有人会往输入框里贴一个10MB的日志文件,这时候在主线程跑json.decode,界面立刻卡死,甚至直接被系统判定无响应。
Flutter针对这种计算密集场景提供了compute,它会把函数抛到后台isolate执行,跑完再把结果传回主线程。使用并不复杂:
dart复制import 'package:flutter/foundation.dart';
Future<String> formatInBackground(String raw) {
return compute(formatJson, raw);
}
注意compute要求传入的函数是顶层的或者是静态方法,不能是实例方法。它的参数和返回值都必须能跨isolate传输,好在String在Dart里天然支持跨isolate传递。处理大文件时,我会配合一个加载状态,界面上显示转圈,处理完再回显结果。实际操作中,1MB左右的JSON在安卓真机上普通解析大约几十毫秒,在OpenHarmony分支上可能稍慢一点,但使用后台isolate后界面丝滑,用户感知完全不一样。
不过也别迷信compute,它每次调用都要创建isolate,本身有开销。判断标准很简单:预料输入会在1MB以下,直接同步解析;超过这个量级,再走后台。工具里最好两个入口都放上,给用户选择。
3.4 保持可读性的编码细节
很多人格式化后会发现中文字符变成了\uXXXX,看起来像乱码。原因在于Dart的JsonEncoder默认会把非ASCII字符转义。这在传输场景下是安全的,但人眼查看时体验极差。
要让格式化结果保持中文可读,可以自定义JsonEncoder:
dart复制String keepUnicodeString(String source) {
final buffer = StringBuffer();
for (final rune in source.runes) {
buffer.write(String.fromCharCode(rune));
}
return buffer.toString();
}
实际操作里还有一种更省事的方式:先json.decode解析,再通过json.encode的toEncodable参数控制。不过对比下来,直接保留中文输出反而更简单,因为dart:convert的utf8.decode能力已经足够。我的实现是封装了一层encodeJsonReadable,内部用JsonEncoder.withIndent生成结果后,再把\\u开头的一连串字符替换成原始字符,既保证结构不变,又让中文显示恢复。这个小细节属于“不做没人骂、做了真香”的类型。
4. 页面交互与操作体验设计
4.1 输入区:粘贴优先,键盘折叠
工具类的核心交互讲究“少点一下”。首页我直接放了一个可拉伸的输入区域,用TextField实现,maxLines设为空,就可以支持超大文本输入和滚动。真机上频繁长按粘贴很反人类,所以我专门加了一个“从剪贴板粘贴”按钮:
dart复制Future<void> pasteFromClipboard() async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data?.text != null) {
_inputController.text = data.text!;
_autoHandle();
}
}
这里有一个细节:Clipboard.getData在OpenHarmony的Flutter分支上不一定像Android那样每次都能拿到内容,偶尔会返回空数据。我会在按钮文字下方给一个小提示,告诉用户如果粘贴失败,手动长按文本框再粘贴一次,避免用户在交互上产生困惑。
另外,处理输入区时我给TextField包了一层Padding和Container,让输入区域在底部让出安全区,避免内容被系统导航条挡住。这个在真机上非常影响体验,别用默认值直接上。
4.2 输出区与统计栏
输出区我没有用直接不可编辑的TextView,而是继续用一个只读的TextField或者SelectableText,目的是让用户可以长按选取部分内容复制。格式化结果往往很长,一键全选复制并不总是用户想要的。
统计栏放在输入输出区之间,实时显示三段信息:字符数、行数、处理耗时。
dart复制final stopwatch = Stopwatch()..start();
final result = formatJson(input);
stopwatch.stop();
setState(() {
_statsText = '${result.length} 字符 / ${result.split('\n').length} 行 / ${stopwatch.elapsedMilliseconds}ms';
});
这里不要小看耗时显示,用户在调试时看到“18ms处理完2万字符”,心里是有底的。而在处理大文件时,耗时数值还能侧面反映是否真的走了后台isolate,是个很有用的调试指标。
4.3 复制反馈与模式切换
格式化、压缩、校验这三个操作我用按钮组排在页面顶部,用户先选模式,再粘贴输入,结果自动出现。校验模式下,成功就展示“JSON合法”,失败就展示错误行、列和原因,不用输出格式化结果,避免页面杂乱。
复制按钮的实现很直接:
dart复制await Clipboard.setData(ClipboardData(text: _outputController.text));
但为了“反馈看得见”,我加了一个短暂的SnackBar提示“已复制到剪贴板”。在OpenHarmony的分支上,Clipboard.setData偶尔会丢数据,最稳的做法是复制前先判断输出文本是否为空,为空直接给提示,不为空再复制。别小看这个判断,空文本复制一次,后面用户粘贴时会怀疑是不是工具坏了。
深色模式也是顺手做的。工具类应用经常在深夜调试时打开,我直接用ThemeMode.system配合Material的darkTheme,没有额外写逻辑,但整个体验质感提升一大截。
5. OpenHarmony鸿蒙适配与平台能力
5.1 什么时候需要EventChannel
JSON格式化本身不依赖任何原生能力,剪贴板走Flutter框架层就够了。但如果你想把工具做成“开发助手全家桶”,比如监听系统剪贴板变化、读取本地文件、访问网络接口,那就要和OpenHarmony原生侧打交道了。
Flutter原生通信有三件套:MethodChannel用于方法调用,EventChannel用于事件推送,BasicMessageChannel用于双向消息。以监听剪贴板变化为例,Dart侧用EventChannel接收原生事件:
dart复制const EventChannel _clipboardChannel = EventChannel('com.example.json_tools/clipboard');
void listenClipboard() {
_clipboardChannel.receiveBroadcastStream().listen((event) {
if (event is String) {
_inputController.text = event;
}
});
}
原生侧在ohos/entry/src/main/ets里通过eventChannel注册事件处理器,把系统剪贴板变化转为Dart事件。这里有个经验:EventChannel的通道名必须唯一,千万别用默认的名字和别的插件撞上。我见过同事因为通道名重复导致事件串台的,排查半天都没反应过来。
5.2 module.json5与权限配置
每个OpenHarmony应用都有一个module.json5,相当于Android的AndroidManifest.xml加build.gradle的集合。如果你的工具后续要访问网络、读写文件,必须在这里声明权限:
json5复制{
"module": {
"name": "entry",
"type": "entry",
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
这里要特别注意,module.json5里声明的权限不是全都立即生效,有些还需要用户在系统设置中授权。剪贴板类的基础操作在当前版本不需要显式权限,但不同API版本之间规则会变,适配时以官方文档为准。我的项目前期还傻傻地给剪贴板加了权限声明,后来发现纯属多余,删掉反而更干净。
5.3 hap打包签名与真机安装流程
在真机上安装hap的方式有两种:DevEco Studio的设备管理窗口直接点击运行,或者命令行用hdc install安装:
bash复制hdc install path/to/app.hap
第一次安装会遇到签名问题。OpenHarmony默认要求应用带签名,否则不给装。DevEco Studio里需要配置自动签名,手机会从开发模式变成“已认证设备”,这一步不复杂,但在连接开发板时最容易卡住——有些板子连接到DevEco后不弹出授权确认,此时去设置 > 开发者选项里手动触发USB调试授权即可。
整个hap产物一般在几十MB到一百MB出头,对OpenHarmony设备来说属于轻量应用。它不像Android那样要处理一堆so库兼容问题,只要Flutter分支版本和OpenHarmony系统API版本匹配,基本就是“打包-安装-运行”三步走。
6. 常见问题与踩坑记录
6.1 Flutter SDK版本不匹配导致的构建报错
很多读者会直接拿原版Flutter创建工程,再塞进OpenHarmony项目的ohos目录里编译,结果一执行就报“The current configured Flutter SDK is not known to be fully supported”。这提示翻译成人话是:插件或框架层不兼容你当前的SDK。
排查顺序建议是:先看flutter --version确认当前SDK是不是OpenHarmony分支;再翻ohos/entry下的build-profile.json5里声明的依赖与SDK版本是否匹配;最后检查pubspec.lock里有没有锁死插件版本。大多数情况下,把SDK切换到OpenHarmony官方分支,并且把分支版本固定下来,这个报错就消失了。
6.2 后端时间类型引发的JSON反序列化报错
调试时经常遇到这样一句话:“json parse error: cannot deserialize value of type `java.util.Date` from string”。这其实不是你App的错,而是后端Java服务在反序列化JSON时,发现时间字段的值和它定义的格式不一致。
作为前端工具,我们能做的是把这种错误理解透并友好展示。比如后端要求的是yyyy-MM-dd HH:mm:ss,结果给的却是ISO8601格式,Java那边就直接炸了。在我们工具里,这类错误会被JSON解析层捕获,但不会影响前面正常的JSON结构检查。如果后续想做更高级的调试,可以加一个“时间戳识别”功能,把常见的epoch millis自动转成可读时间,帮用户快速判断是格式问题还是数据问题。
6.3 大JSON处理的性能陷阱
同步解析大JSON卡死UI,是最常见的性能问题。第一次我把解析逻辑直接放在TextField的onChanged回调里,粘贴2MB数据后App直接无响应,OpenHarmony系统弹了强停提示。换成compute之后,界面丝滑,但又要处理另一个问题:连续快速粘贴时,多个isolate并发会创建大量线程,导致系统短暂高负载。
后来我加了一个“防抖”逻辑:用户停止输入500毫秒后才触发解析,解析过程中如果有新输入进来,取消上一次结果。防抖用Timer实现:
dart复制Timer? _debounce;
void _onInputChanged(String value) {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
handleJson();
});
}
这个改动属于“不写不会出事,写了体验翻倍”的典型。工具类应用的用户输入节奏很碎,防抖是最基本的人性化处理。
6.4 剪贴板适配差异与中文编码问题
OpenHarmony的剪贴板实现和Android不完全一致。我在测试中发现,部分版本下Clipboard.getData(Clipboard.kTextPlain)获取到的内容偶尔为空,这是平台通道的适配问题,不是Dart侧代码的问题。临时方案是引导用户手动粘贴,长期方案则是通过EventChannel自己实现剪贴板读取。
中文编码方面,除了\uXXXX转义问题,还有一个比较隐蔽的坑:如果输入内容带了BOM头,json.decode会直接抛异常。处理办法是在解析前做一次trim加BOM剥离:
dart复制String removeBomIfExists(String input) {
if (input.isNotEmpty && input.codeUnitAt(0) == 0xFEFF) {
return input.substring(1);
}
return input;
}
这个细节很多人一辈子都碰不到,但只要用户从Windows记事本里复制过JSON,就一定会踩到。
6.5 其他高频问题速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 格式化输出中文变成\u转义 | JsonEncoder默认转义非ASCII | 自定义Encoder或转义替换 |
| 输入带BOM导致解析失败 | UTF-8 BOM未去除 | 解析前剥离BOM头 |
| 长按粘贴在真机上失效 | 平台剪贴板适配异常 | 增加一键粘贴按钮兜底 |
| 构建hap时SDK路径报错 | local.properties路径不对 | 检查本地SDK路径配置 |
| Flutter渲染出现异常图片 | 渲染引擎与系统版本不兼容 | 尝试关闭Impeller或切换分支 |
| 大JSON卡死界面 | 主线程执行解析 | 改用compute后台处理 |
| 时间字段解析报错 | 后端格式定义和实际不一致 | 工具侧做错误提示,不强行修复 |
写个小结:工具类App的实用主义
这个项目从立项到跑通,差不多用了两个晚上加一个周末,真正核心的代码量不到五百行。我在实际使用中发现,一个工具类应用最重要的不是功能多,而是“每一个功能都顺手”。JSON格式化、压缩、校验这三件事看起来简单,真正把它们组合成一条顺畅的操作流,需要花在设计交互上的心思反而比写解析逻辑多得多。
如果你也想做一个OpenHarmony上的开发助手,建议从这个小切口入手,先跑通Flutter到hap的整套流程,再去想增加SQL格式化、Base64编解码、时间戳转换甚至二维码生成这些后续功能。我个人后面就在往这个方向扩展,代码一点都不浪费,同一个输入框、同一套结果展示逻辑,换个解析函数就能多一个工具。这种一点点搭积木的成就感,可能是做工具类App最大的回报。
