引子
前后端数据交互多用Json,比较好用的 json解析东西或许结构,比方:web版别的 jsonToDart,IDE版别的 jsonToDartBeanAction,根本都能满足惯例需求,可是在某些特别的项目场景之下,比方,咱们自建了一套dart版的网络集成结构,其间需求一个 独自的 静态decode函数,用于将map直接转成 目标。假如依然用 jsonToDart那么每一次生成的json都需求手动创立decode办法,比较费事。
Flutter在桌面端的落地,让移动开发者定制自己的JSON东西成为或许,支撑所有的PC端,包含前端开发常用的windows,mac。
首要内容为:从0开端构建一个PC下的FlutterJSON解析东西的全进程,将JSON转化东西 必需的功用,开发中遇到的问题,对应的解决方案,以及 Flutter的PC端生态现状 经过图文展现出来。
本案例在windows平台下进行试验,运用flutterSDK版别为 3.0.1 。
作用展现
下图为静态图:
界面参照了 市面上的 json转化东西经过优化调整改造而成:
- 左上角为原始文件输入框
- 左下方为json格式化而且高亮显现区域
- 右侧输出框为 生成dart文件内容
- 中心部分操作按钮
功用架构
以上4个区域,包含的所有功用点一览:
-
PC风格窗口办理
- 定制操作窗口的可缩放最大最小尺度
- 彻底自定义的窗口款式(包含最小化,最大化,封闭按钮的自定义,边框的自定义,头部支撑拖动)
- 鼠标悬停时的提示框
-
导入/导出 PC文件
- 一键读取网络文件
- 一键读取本地文件
- 一键复制dart文件内容
- 读取拖拽文件内容
- 导出dart文件到本地
-
Json格式化与高亮
- json语法错误查看
- 缩进和换行的格式化
- json部分字段高亮显现
-
Json转化为 Dart文件
- json递归遍历生成多个dart类
- dart类的部分代码高亮显现
比较于网页版的jsonToDart,本东西修正了jsonToDart的语法lint正告,而且支撑自定义生成dart函数,其他功用与jsonToDart一样。
下文将分章节讲述功用的完成。
PC风格窗口办理
PC与移动端的操作习惯彻底不同,最显着的一点便是窗口办理,移动端一般都是全屏运用不行缩放,而 PC端,多为指定一个最小宽高保证UI正常显现,一起支撑缩放到全屏幕,一般右上角还会存在最小化,最大化和封闭按钮的东西栏。
Flutter在PC开发上的生态近期还算完善,关于PC风格界面办理的库,运用比较广泛的是 window_manager 和 bitsdojo_window,两者平起平坐,对比了一下运用难度,发现 后者不仅支撑 窗口拖拽,而且东西栏还支撑彻底自定义,通用性相对较好,而前者没有发现相关材料,于是本案例挑选了后者。
运用方法如下:
引进依靠库
bitsdojo_window: ^0.1.1
特别注意
运用 bitsdojo_window 之后有必要修正 widows目录下的 main.cpp文件 ,引进一个头文件以及一行代码, 不然自定义窗口会失效。
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
定制窗口尺度
void main() {
runApp(const MyApp());
doWhenWindowReady(() {
const initialSize = Size(1220, 600);// 设定初始值
appWindow.minSize = initialSize; // 缩放时不能小于这个值
appWindow.size = initialSize;// 翻开运用时的默许巨细
appWindow.alignment = Alignment.center;// 窗口对齐方法
appWindow.show();
});
}
定制边框款式
下面代码中的 WindowBorder 是 bitsdojo_window库供给的边框。仅支撑 边框的色彩和厚度。原本我预想是否能够支撑到边框的形状圆角,尝试了一番发现并不支撑,即使我修正 源代码也无法做到。猜测或许是PC生态中制止了这一行为。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return OKToast(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blueGrey),
home: Scaffold(
body: WindowBorder(color: Colors.blueGrey, width: 2, child: Column(children: const [WindowTopBox(), MainPage()])),
),
));
}
}
定制最小化,最大化,封闭的操作栏
以下代码重视两个点:榜首是 MoveWindow,flutter打出pc包时,会默许带上对应体系自带的窗口头部,包含标题以及3个操作按钮,并支撑窗口在非全屏时的拖动。而 bitsdojo_window首先是禁用了 体系默许的头部,然后供给了 MoveWindow 供给拖动作用。
第二,是 3个操作按钮 MinimizeWindowButton, MaximizeWindowButton,CloseWindowButton 由 bitsdojo_window 供给默许款式,假如不喜欢 本来的款式,还能够 自己做一个组件,而且运用 appWindow.appWindow.minimize()
的操作函数进行彻底化的自定义。
Color _mainColor = Colors.blueGrey;
/// 顶部操作按钮
class WindowTopBox extends StatelessWidget {
const WindowTopBox({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget current;
current = WindowTitleBarBox(
child: Row(children: [
Expanded(
child: Container(
color: _mainColor,
child: MoveWindow(
child: Row(children: const [
SizedBox(width: 20),
Text(
'Json解析东西',
style: TextStyle(fontWeight: FontWeight.w700, color: Colors.white),
)
])))),
_WindowButtons()
]));
return current;
}
}
class _WindowButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(children: [
MinimizeWindowButton(),
MaximizeWindowButton(),
CloseWindowButton(),
]);
}
}
鼠标悬停时的提示框
PC上特有操作,光标悬停在组件上方时,有时分需求显现一些提示:
咱们需求自定义 鼠标悬停时显现浮层,鼠标脱离时 浮层消失的Widget。参阅代码如下:
import 'package:flutter/material.dart';
/// 鼠标放置上去会显现提示框的组件,底层显现的子组件和 弹窗显现的组件都必传
/// 提示框的方位会跟随组件方位而改变
class HoverEventWidget extends StatefulWidget {
final Widget showChild; // 原组件
final Widget floatWidget; // 浮层组件
final bool showDown; // 是否显现在原组件的下方
const HoverEventWidget({Key? key, required this.showChild, required this.floatWidget, required this.showDown}) : super(key: key);
@override
State<StatefulWidget> createState() {
return HoverEventWidgetState();
}
}
class HoverEventWidgetState extends State<HoverEventWidget> {
bool showTipBool = false; // true 窗口已弹出,false窗口未弹出
OverlayEntry? overlay;
final GlobalKey _keyGreen = GlobalKey();
@override
Widget build(BuildContext context) {
return InkWell(
key: _keyGreen,
hoverColor: Colors.white,
highlightColor: Colors.white,
splashColor: Colors.white,
onHover: (bool value) {
if (value == true) {
showTipWidget(context);
} else {
dismissDialog();
}
},
onTap: () {},
child: widget.showChild,
);
}
void dismissDialog() {
overlay?.remove();
showTipBool = false;
}
/// 让这个办法支撑屡次调用,假如已经显现了,再次调用显现,则不与反响
void showTipWidget(BuildContext context) {
if (showTipBool) {
return;
}
showTipBool = true;
final RenderBox box = _keyGreen.currentContext?.findRenderObject() as RenderBox;
Offset offset = box.localToGlobal(Offset.zero);
OverlayEntry overlay = OverlayEntry(builder: (_) {
return Positioned(
left: offset.dx,
top: widget.showDown ? offset.dy + 30 : offset.dy - box.size.height - 30,
child: widget.floatWidget,
);
});
Overlay.of(context)?.insert(overlay);
this.overlay = overlay;
}
}
以上代码重视几个要素:
-
InkWell
onHover函数能够响应鼠标悬停以及脱离的事件,可是要特别注意一个坑,运用 onHover 之后,onTab函数必需不为空,不然 onHover也无效。原因不明。
-
Overlay
Overlay是Flutter中的浮层组件,支撑窗口多级分层。悬浮组件用Overlay刚刚好。
-
GlobalKey
显现悬浮组件时存在一个方位问题,咱们往往想悬浮层显现在组件的邻近,比方上方和下方,可是条件是咱们有必要要能够取得组件的方位和巨细。当咱们用 一个 GlobalKey 标记了一个widget之后,就能选用
final RenderBox box = _keyGreen.currentContext?.findRenderObject() as RenderBox;
获取组件在运行时的宽高(
box.size.height
) 方位 (Offset offset = box.localToGlobal(Offset.zero);
)。
导入/导出 PC文件
一键读取网络文件
引进 dio: ^4.0.6
, 弹窗要求输入 网络文件地址,运用 dio读取文件内容
一键读取本地文件
引进 file_picker: ^4.4.0
,运用办法pickFiles挑选本地文件:
FilePickerResult? value = await FilePicker.platform.pickFiles();
一键复制dart文件内容
Flutter自带的 Clipboard 能够直接办理剪切板,无需引进其他依靠库。
Clipboard.setData(ClipboardData(text: widget.textContent));
读取拖拽文件内容
引进 desktop_drop: ^0.3.3
运用 DropTarget 组件包裹本来的主布局,而且完成几个关键函数即可。
@override
Widget build(BuildContext context) {
return Expanded(
child: DropTarget(
onDragDone: (detail) { // 拖拽完成
setState(() {
_list.clear();
// 只能接纳一个文件的拖拽
if (detail.files.length > 1) {
showToast('只能一起解析一个文件');
} else {
_list.addAll(detail.files);
readDraggedFile(_list[0]);
}
});
},
onDragEntered: (detail) { // 拖拽进入
setState(() {
_dragging = true;
});
},
onDragExited: (detail) { // 拖拽脱离
setState(() {
_dragging = false;
});
},
child: Container(// 主布局
color: Colors.grey.shade200,
padding: const EdgeInsets.all(10.5),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
buildLeft(),
buildMid(),
buildRight(),
]),
),
),
);
}
导出dart文件到本地
先运用上面的FilePicker挑选本地文件夹,然后用File直接写入文件即可。
ElevatedButton(
onPressed: () async {
String? path = await FilePicker.platform.getDirectoryPath();// 挑选目录
if (path == null) {
return;
}
File f = File('$path/${widget.clzName}.dart');
f.writeAsString(widget.textContent);
showToast('生成桌面文件成功 ${f.path}');
},
child: const Text('导出文件到'))
小结
PC上的文件操作咱们彻底无需关心完成方法,可见在这方面Flutter的生态还是比较完善的。可是MAC上或许涉及到 文件权限,有别的的编码本钱。
Json格式化与高亮
json语法错误查看
当产生json的语法性错误时,咱们需求将错误展现给用户。完成的方法比想象中简略,其实咱们只需求尝试对 json进行 jsonDecode即可。
咱们供给一个 String的扩展函数:
extension JSONHelper on String? {
String get jsJSON {
if (this == null) return 'ERROR: 内容为空';
try {
jsonDecode(this ?? '');
} catch (e) {
return 'ERROR: $e';
}
return '';
}
}
其间,运用 jsonDecode进行解析,它解析的结果或许是一个Map目标,或许一个List目标,假如解析中出现反常,经过try catch能够捕获,咱们直接将反常抛出即可。
缩进和换行的格式化
经过上面的语法查看之后,json要经过格式化才干更便利阅读。
参阅代码如下:
这是一个递归函数,三个入参的含义分别为:
-
object
将要转化的目标,或许的类型包含: String,num,bool,Map以及List。其间比较杂乱的场景,为 map和list彼此嵌套的状况。比方,list的元素类型便是Map,或许Map中某个特点值为 List类型。这时分就会涉及到递归调用,经过屡次缩进来分解层级。
简略类型,则只需求将String,num,或许bool的值,添补在key后面即可。
最终,假如后端给的某个key对应的value是null,相同咱们也用null作为兜底。
-
deep
当时层级,每一次递归,层级+1,文字缩进也多一层
-
isObject
榜首个参数object是否来自特点值(最外层的目标首行不需求缩进,而内层目标则需求缩进)。
String convert(dynamic object, int deep, {bool isObject = false}) {
var buffer = StringBuffer();
var nextDeep = deep + 1;
if (object is String) {
//为字符串时,需求增加双引号并回来当时内容
buffer.write(""$object"");
return buffer.toString();
}
if (object is num || object is bool) {
//为数字或许布尔值时,回来当时内容
buffer.write(object);
return buffer.toString();
}
if (object is Map) {
var list = object.keys.toList();
if (isObject) {
buffer.write(space1());
}
buffer.write("{");
if (list.isEmpty) {
//当map为空,直接回来‘}’
buffer.write("}");
} else {
buffer.write("\n");
for (int i = 0; i < list.length; i++) {
buffer.write("${getDeepSpace(nextDeep)}"${list[i]}":");
buffer.write(convert(object[list[i]], nextDeep, isObject: true));
if (i < list.length - 1) {
buffer.write(",");
buffer.write("\n");
}
}
buffer.write("\n");
buffer.write("${getDeepSpace(deep)}}");
}
return buffer.toString();
}
if (object is List) {
if (isObject) {
buffer.write(space1());
}
buffer.write("[");
if (object.isEmpty) {
//当list为空,直接回来‘]’
buffer.write("]");
} else {
buffer.write("\n");
for (int i = 0; i < object.length; i++) {
buffer.write(getDeepSpace(nextDeep));
buffer.write(convert(object[i], nextDeep));
if (i < object.length - 1) {
buffer.write(",");
buffer.write("\n");
}
}
buffer.write("\n");
buffer.write("${getDeepSpace(deep)}]");
}
return buffer.toString();
}
//假如目标为空,则回来null字符串
buffer.write("null");
return buffer.toString();
}
json部分字段高亮显现
json除了要格式化之外,为了明晰地看到字段地层级结构,最好是能够用色彩区分key和value,以及不同类型的value运用不同的色彩。
在dart中,textSpan这个概念能够支撑 同一个 文本目标的各个部分具有不同的风格。它的运用方法大约如下:
Text.rich(TextSpan(text: '本身的案牍内容和风格',style: TextStyle(color:Colors.lime),children: [
TextSpan(text: '子span',style: TextStyle(color:Colors.red))
])),
首要特点为:
text,style : 本身的案牍内容和风格。
children: 子span(相同支撑风格)
展现的作用如下:
子span会跟随在本身内容之后。所以假如咱们需求拼接的话,就只需求将 要拼接的内容放在children中。
能够运用flutter支撑的 运算符重载的 特性,让 拼接上写法大大简化。
extension TextSpanHelper on TextSpan {
TextSpan operator +(TextSpan textSpan) {
return TextSpan(children: [this, textSpan]);
}
}
完好的参阅代码如下:
相同是递归函数,递归仅产生在object类型为map和list的时分。所有入参和上一末节一样。
TextSpan getFormattedJsonSpan(dynamic object, int deep, {bool isObject = false}) {
TextSpan box = const TextSpan();
var nextDeep = deep + 1; // 每次递归,层级都会+1
if (object is Map) {
if (object.isEmpty) {
box += TextSpan(text: ' { }', style: getTextStyleByColor(color: Colors.black));
return box;
}
if (isObject) {
box += TextSpan(text: space1());
}
box += TextSpan(text: '{\n', style: getTextStyleByColor(color: Colors.black));
List list = object.keys.toList();
for (int i = 0; i < list.length; i++) {
var k = list[i];
var v = object[k];
box += TextSpan(text: getDeepSpace(nextDeep));
box += TextSpan(text: '"$k"', style: getTextStyleByColor(color: Colors.lightGreen));
box += TextSpan(text: ':', style: getTextStyleByColor(color: Colors.black));
box += getFormattedJsonSpan(v, nextDeep, isObject: true);
if (i < list.length - 1) {
box += TextSpan(text: ',\n', style: getTextStyleByColor(color: Colors.black));
}
}
if (isObject) {
box += TextSpan(text: '\n${getDeepSpace(nextDeep - 1)}}', style: getTextStyleByColor(color: Colors.black));
} else {
box += TextSpan(text: '\n}', style: getTextStyleByColor(color: Colors.black));
}
return box;
}
if (object is List) {
if (object.isEmpty) {
box += TextSpan(text: ' [ ]', style: getTextStyleByColor(color: Colors.black));
return box;
}
if (isObject) {
box += TextSpan(text: space1());
}
box += TextSpan(text: '[\n', style: getTextStyleByColor(color: Colors.black));
for (int i = 0; i < object.length; i++) {
box += TextSpan(text: getDeepSpace(nextDeep));
box += getFormattedJsonSpan(object[i], nextDeep, isObject: true);
if (i < object.length - 1) {
box += TextSpan(text: ',\n', style: getTextStyleByColor(color: Colors.black));
}
}
if (isObject) {
box += TextSpan(text: '\n${getDeepSpace(nextDeep - 1)}}', style: getTextStyleByColor(color: Colors.black));
} else {
box += const TextSpan(text: '\n}', style: TextStyle(color: Colors.black));
}
return box;
}
if (object is String) {
//为字符串时,需求增加双引号并回来当时内容
box += TextSpan(text: ' "$object"', style: getTextStyleByColor(color: Colors.blue));
return box;
}
// num下就只有int和double
if (object is num) {
box += TextSpan(text: ' $object', style: getTextStyleByColor(color: Colors.redAccent));
return box;
}
if (object is bool) {
box += TextSpan(text: ' $object', style: getTextStyleByColor(color: Colors.lightGreen));
return box;
}
// num下就只有int和double
box += TextSpan(text: 'null', style: getTextStyleByColor(color: Colors.cyan));
return box;
}
经过这个函数的处理,咱们就得到了高亮之后的json:
Json转化为 Dart文件
一个用于业务开发的entity类,一般包含如下部分,
- 成员变量区
- 结构函数区
- FromJson函数区
- toJson函数区
- 自定义函数区
上面4个是通用的,而自定义函数区是在运用方有特别要求时,能够按要求参加特别的代码进去。此时我需求一个decode函数用于第三方网络结构去运用。所以 这儿的自定义函数便是decode。
生成的dart文件大约用作两类,榜首:经过文本的方法复制,或许导出到PC本地,第二,现场阅读。前者有必要是 字符串的形式导出,而后者,为了阅读的便利明晰,相同需求经过textSpan的高亮作用将重要的环节醒目处理。
json递归遍历生成多个dart类
核心函数的脉络如下:
static String trans(Map map, {required String className}) {
StringBuffer sb = StringBuffer();
try {
// 类头
sb.writeln('class $className {\n');
// 成员特点区
FieldParserResult fieldParserResult = _fieldArea(map);
sb.writeln(fieldParserResult.fields);
// 结构函数区
sb.writeln(_constructorFunctionArea(map, className));
// fromJson函数
sb.writeln(_fromJsonFunctionArea(map,className));
// toJson函数区
sb.writeln(_toJsonFunctionArea(map));
// decode函数
sb.writeln(_decodeFunctionArea(map,className));
sb.writeln('}\n\n');
// 生成相关实体类
for (var e in fieldParserResult.clzs) {
sb.writeln(e);
}
} catch (e) {
rethrow;
}
return sb.toString();
}
json类的生成作用参阅了比较权威的 jsonToDart网站,可是它生成的类有一些语法正告,随手修正了一些正告之后,完成了这一步的转化工作。
有必要注意的是,json转dart,要考虑生成多级 实体类的状况,假如一个key对应的value是杂乱类型Map时, 或许 value是List,而且list的泛型是 Map时,即 如下两种状况 :
{
"m": {
"a":1
},
"m2": [
{
"a":1
}
]
}
所以一个完好的jsonToDart函数,一定是一个递归函数,递归的进程,产生在 解析 类特点的进程中,即上方的 _fieldArea 函数。
dart类的部分代码高亮显现
思路同上一届类似,只不过把String替换成 TextSpan。
static TextSpan trans(Map map, {required String className, required bool needDecodeFunction}) {
TextSpan ts = const TextSpan();
try {
// 类头
ts += const TextSpan(text: 'class ');
ts += TextSpan(text: className, style: classNameStyle);
ts += const TextSpan(text: ' {\n');
// 成员特点区
FieldParserTextSpanResult fieldParserResult = _fieldArea(map, needDecodeFunction);
ts += fieldParserResult.fields;
// 结构函数区
ts += _constructorFunctionArea(map, className);
// fromJson函数
ts += _fromJsonFunctionArea(map, className);
// toJson函数区
ts += _toJsonFunctionArea(map);
if (needDecodeFunction) {
// decode函数
ts += _decodeFunctionArea(map, className);
}
ts += const TextSpan(text: '\n}\n\n');
// 生成相关实体类
for (var e in fieldParserResult.clzs) {
ts += e;
}
} catch (e) {
rethrow;
}
return ts;
}
最终生成的 TextSpan目标经过 Text.rich填充到UI上即可。
作用如下:
完好代码请重视文末,代码可运行。
总结
写完一套东西下来,最大的感触便是,用Flutter写出来的PC运用,严格来说不是传统含义上的PC运用,而是 移动运用在PC终端上展现。
原因如下:
-
PC端很常见的多窗口模式,就像某IM的PC端:登录的小窗,接上 主界面大窗,独自私聊的小窗。
现在Flutter没有找到这种作用的官方支撑。
2. PC上还有把运用隐藏到右下角小图标的操作,也没有找到官方支撑。
- 在登录时,咱们一般会用PC的键盘回车,来替代鼠标点击登录按钮,Flutter官方也尚不支撑。
Flutter现在已知能够支撑的PC运用的特性包含但不限于:
- 鼠标放置的作用:hover ,当鼠标光标放置在组件上时,需求显现 起浮组件。
- 本地文件挑选
3. PC端的装置进程。一般PC上的软件有两种方法能够装置,一个是绿色免装置包,复制进来直接就能用,一个是装置包,双击解压,装置到磁盘指定目录。
上面是官网阐明,确实是支撑,不过现在本人还未尝试过。
参阅代码
完好的参阅代码在 github.com/18598925736…
有关Flutter PC端开发的问题欢迎留言评论。