Flutter for OpenHarmony 文件转换助手App实战 - JSON格式化
结构]入口方法写薄,弹窗组件自己管状态[体验]美化/压缩切换清晰,复制/清空一键可用[健壮]空输入拦截、解析失败提示、controller 记得释放后面如果你要继续扩展(比如 JSON 排序、转义/反转义、提取字段),也可以沿用这套思路:每个工具都尽量是一个独立的小组件,最后在工具列表里拼起来。欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.

JSON 格式化算是我在“工具箱类 App”里最早加的一个功能:需求简单,但能把输入校验、状态管理、交互细节都串起来。
这个小工具我一般会做到这几件事:
- [美化] 把一行 JSON 按缩进排版,方便阅读
- [压缩] 把带缩进/换行的 JSON 压成一行,方便粘贴到日志或接口参数
- [校验] 输入不合法时给出明确提示(别让用户猜哪里错了)
- [便捷操作] 一键复制结果、一键清空输入
JSON格式化工具的应用
JSON格式化在API开发和数据处理中非常重要。开发者经常需要验证JSON数据的有效性,或者将压缩的JSON数据进行美化以便阅读。
在工具页面中,我是把 JSON 格式化当成一个普通的工具项挂到列表里。这里不建议把配置写得太复杂,能读懂、能扩展就行。
{'icon': Icons.data_object, 'title': 'JSON格式化'},
说明
这里的关键点其实不是图标和标题,而是你后面“点了这一项要做什么”。我习惯再配一个 onTap 或 toolKey,避免后期靠 title 字符串去判断(容易踩坑:改文案就把逻辑改炸了)。
JSON格式化对话框的实现
用户点击“JSON 格式化”后,我这里用对话框承载交互,原因很现实:
- [场景短] 格式化通常是一次性操作,不需要专门开一个页面
- [实现快] 复用现有页面结构,少改路由
入口方法我会尽量写薄一点,只负责打开弹窗,把状态交给一个独立组件。
void _showJsonFormatterDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => const _JsonFormatterDialog(),
);
}
说明
showDialog 里如果塞太多 UI,很快就会变成“超长方法”。我这里把真实交互抽成 _JsonFormatterDialog,后面要加“复制”“清空”“模式切换”都不会把入口方法越写越胖。
接下来是弹窗主体。这个组件需要维护输入/输出文本、当前模式,以及按钮点击后的状态刷新,所以我用 StatefulWidget。
class _JsonFormatterDialog extends StatefulWidget {
const _JsonFormatterDialog();
State<_JsonFormatterDialog> createState() => _JsonFormatterDialogState();
}
enum _JsonMode { pretty, compact }
说明
- [模式枚举] 用
_JsonMode比用bool isPretty更直观,后面如果加“排序 key”“保留 unicode”这种开关也好扩展。 - [私有组件] 前面加下划线是为了把实现细节限定在当前文件里,减少全局污染(工具页面越做越多时很有用)。
状态类里我会把 TextEditingController 放在一起管理,并且记得释放(这个点在工具类弹窗里很容易被忽略)。
class _JsonFormatterDialogState extends State<_JsonFormatterDialog> {
final _inputCtrl = TextEditingController();
final _outputCtrl = TextEditingController();
_JsonMode _mode = _JsonMode.pretty;
void dispose() {
_inputCtrl.dispose();
_outputCtrl.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('JSON格式化'),
content: _buildContent(context),
actions: _buildActions(context),
);
}
}
说明
- [控制器] 输入/输出各一个 controller,输出框设为只读,避免用户误改。
- [释放资源] 这种弹窗经常被频繁打开,
dispose()不做的话 controller 会一直挂着,时间久了内存和监听堆积很明显。
内容区我会按“模式切换 + 输入 + 输出”的顺序排,保证用户一眼知道先做什么。
Widget _buildContent(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildModeSwitch(),
SizedBox(height: 12.h),
TextField(
controller: _inputCtrl,
maxLines: 6,
decoration: InputDecoration(
hintText: '输入JSON',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
SizedBox(height: 12.h),
TextField(
controller: _outputCtrl,
readOnly: true,
maxLines: 6,
decoration: InputDecoration(
hintText: '结果输出',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
],
),
);
}
说明
我把“输入”和“输出”都做成多行文本框,主要是适配两种情况:
- [输入很长] 接口返回的 JSON 可能一大坨,必须能滚动
- [输出要复制] 输出框用
controller,复制时直接读_outputCtrl.text,不用到处传值
JSON解析和验证
JSON格式化的第一步是解析JSON数据,验证其有效性。在Dart中,我们可以使用jsonDecode函数来解析JSON字符串:
import 'dart:convert';
说明
这块我一般只引入 dart:convert,避免为了格式化 JSON 额外上三方包。工具类功能越多,依赖越少越稳。
解析和转换我会拆成一个动作方法 _run(),点“执行”时调用。失败不把异常直接怼到 UI 上,而是给一个更友好的提示。
void _run() {
final input = _inputCtrl.text.trim();
if (input.isEmpty) {
_toast(context, '先粘贴一段 JSON 再执行');
return;
}
try {
final jsonData = jsonDecode(input);
final result = _mode == _JsonMode.pretty
? const JsonEncoder.withIndent(' ').convert(jsonData)
: jsonEncode(jsonData);
setState(() {
_outputCtrl.text = result;
});
} catch (_) {
_toast(context, 'JSON 解析失败:请检查引号、逗号和括号是否匹配');
}
}
说明
- [trim] 用户从日志里复制内容时,经常会带换行/空格,先
trim()能少一些“看不懂的错误”。 - [异常信息] 生产工具里我通常不会直接展示
$e,一是信息太长,二是对用户帮助有限;更实用的是告诉他“先检查哪些点”。
JSON美化的实现
JSON美化是指将压缩的JSON数据进行格式化,添加缩进和换行符,使其更易于阅读。JsonEncoder.withIndent方法可以指定缩进的字符串。
说明
JsonEncoder.withIndent(' ') 这里我固定用两个空格,是因为移动端屏幕小,四个空格会把层级挤得很难看。如果你希望更紧凑,可以改成一个空格,甚至换成 \t。
JSON压缩的实现
与美化相反,JSON压缩是指将格式化的JSON数据进行压缩,移除所有不必要的空格和换行符。
说明
压缩我没有再单独写 compressJson(),而是合并进 _run():同一份输入,按 _mode 输出不同结果。这样入口更少,用户也不需要理解“两个按钮两套逻辑”。
双向转换的支持
JSON格式化工具应该支持美化和压缩两种模式。用户可以通过选择不同的模式来切换功能。
Widget _buildModeSwitch() {
return Row(
children: [
Expanded(
child: SegmentedButton<_JsonMode>(
segments: const [
ButtonSegment(value: _JsonMode.pretty, label: Text('美化')),
ButtonSegment(value: _JsonMode.compact, label: Text('压缩')),
],
selected: {_mode},
onSelectionChanged: (s) {
setState(() => _mode = s.first);
},
),
),
],
);
}
说明
- [交互直觉] 两段式切换比下拉框更像“工具开关”,用户不需要多思考。
- [状态联动] 我这里切换模式不自动执行,是刻意的:有些人只想先切换再粘贴内容,自动执行反而会打断输入。
按钮区我会把“执行 / 复制 / 清空 / 关闭”这几个动作放齐,是真正用起来最顺手的组合。
List<Widget> _buildActions(BuildContext context) {
return [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
TextButton(
onPressed: _clear,
child: const Text('清空'),
),
TextButton(
onPressed: _copyResult,
child: const Text('复制'),
),
FilledButton(
onPressed: _run,
child: const Text('执行'),
),
];
}
说明
- [执行] 我用
FilledButton让主操作更突出,用户不会找半天。 - [复制/清空] 工具类弹窗最常用的就是这俩;没有它们,体验会很“半成品”。
错误处理和提示
当用户输入无效的JSON数据时,系统应该显示清晰的错误消息,帮助用户快速定位问题。
void _toast(BuildContext context, String msg) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(msg)));
}
说明
我这里用 SnackBar 是因为它不挡住输入框,用户还能继续改 JSON。弹窗里用 AlertDialog 再套 AlertDialog 反而会让人烦躁。
清空和复制两个动作也顺手补一下实现,不然文章读到这里很难直接照搬落地。
void _clear() {
setState(() {
_inputCtrl.clear();
_outputCtrl.clear();
});
}
说明
清空我同时清输入和输出,避免“输出还是旧的”让用户误以为执行成功。
import 'package:flutter/services.dart';
void _copyResult() {
final text = _outputCtrl.text;
if (text.isEmpty) {
_toast(context, '还没有结果可复制');
return;
}
Clipboard.setData(ClipboardData(text: text));
_toast(context, '已复制到剪贴板');
}
说明
- [空结果提示] 这是很“人”的细节:用户点了复制没反应,会以为 App 卡了。
- [不强依赖平台]
Clipboard这块在 OpenHarmony/多端场景里通常都能走 Flutter 的通用实现,兼容性比自己写平台通道省心。
总结
JSON 格式化这个功能看起来很小,但把它做“顺手”其实靠的是一堆细节:
- [结构] 入口方法写薄,弹窗组件自己管状态
- [体验] 美化/压缩切换清晰,复制/清空一键可用
- [健壮] 空输入拦截、解析失败提示、controller 记得释放
后面如果你要继续扩展(比如 JSON 排序、转义/反转义、提取字段),也可以沿用这套思路:每个工具都尽量是一个独立的小组件,最后在工具列表里拼起来。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)