引子

前后端数据交互多用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

作用展现

下图为静态图:

Flutter桌面版Json解析工具设计
界面参照了 市面上的 json转化东西经过优化调整改造而成:

  1. 左上角为原始文件输入框
  2. 左下方为json格式化而且高亮显现区域
  3. 右侧输出框为 生成dart文件内容
  4. 中心部分操作按钮

功用架构

以上4个区域,包含的所有功用点一览:

  1. PC风格窗口办理

    • 定制操作窗口的可缩放最大最小尺度
    • 彻底自定义的窗口款式(包含最小化,最大化,封闭按钮的自定义,边框的自定义,头部支撑拖动)
    • 鼠标悬停时的提示框
  2. 导入/导出 PC文件

    • 一键读取网络文件
    • 一键读取本地文件
    • 一键复制dart文件内容
    • 读取拖拽文件内容
    • 导出dart文件到本地
  3. Json格式化与高亮

    • json语法错误查看
    • 缩进和换行的格式化
    • json部分字段高亮显现
  4. Json转化为 Dart文件

    • json递归遍历生成多个dart类
    • dart类的部分代码高亮显现

比较于网页版的jsonToDart,本东西修正了jsonToDart的语法lint正告,而且支撑自定义生成dart函数,其他功用与jsonToDart一样。

下文将分章节讲述功用的完成。

PC风格窗口办理

PC与移动端的操作习惯彻底不同,最显着的一点便是窗口办理,移动端一般都是全屏运用不行缩放,而 PC端,多为指定一个最小宽高保证UI正常显现,一起支撑缩放到全屏幕,一般右上角还会存在最小化,最大化和封闭按钮的东西栏。

Flutter在PC开发上的生态近期还算完善,关于PC风格界面办理的库,运用比较广泛的是 window_managerbitsdojo_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();
  });
}

定制边框款式

下面代码中的 WindowBorderbitsdojo_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个操作按钮 MinimizeWindowButtonMaximizeWindowButtonCloseWindowButtonbitsdojo_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上特有操作,光标悬停在组件上方时,有时分需求显现一些提示:

Flutter桌面版Json解析工具设计

咱们需求自定义 鼠标悬停时显现浮层,鼠标脱离时 浮层消失的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;
  }
}
​

以上代码重视几个要素:

  1. InkWell

    onHover函数能够响应鼠标悬停以及脱离的事件,可是要特别注意一个坑,运用 onHover 之后,onTab函数必需不为空,不然 onHover也无效。原因不明。

  2. Overlay

    Overlay是Flutter中的浮层组件,支撑窗口多级分层。悬浮组件用Overlay刚刚好。

  3. 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读取文件内容

Flutter桌面版Json解析工具设计

一键读取本地文件

引进 file_picker: ^4.4.0,运用办法pickFiles挑选本地文件:

FilePickerResult? value = await FilePicker.platform.pickFiles();

Flutter桌面版Json解析工具设计

一键复制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语法错误查看

Flutter桌面版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(相同支撑风格)

展现的作用如下:

Flutter桌面版Json解析工具设计

子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; // 每次递归,层级都会+1if (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:

Flutter桌面版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桌面版Json解析工具设计

完好代码请重视文末,代码可运行。

总结

写完一套东西下来,最大的感触便是,用Flutter写出来的PC运用,严格来说不是传统含义上的PC运用,而是 移动运用在PC终端上展现。

原因如下:

  1. PC端很常见的多窗口模式,就像某IM的PC端:登录的小窗,接上 主界面大窗,独自私聊的小窗。

Flutter桌面版Json解析工具设计

现在Flutter没有找到这种作用的官方支撑。

2. PC上还有把运用隐藏到右下角小图标的操作,也没有找到官方支撑。

Flutter桌面版Json解析工具设计

  1. 在登录时,咱们一般会用PC的键盘回车,来替代鼠标点击登录按钮,Flutter官方也尚不支撑。

Flutter现在已知能够支撑的PC运用的特性包含但不限于:

  1. 鼠标放置的作用:hover ,当鼠标光标放置在组件上时,需求显现 起浮组件。

Flutter桌面版Json解析工具设计

  1. 本地文件挑选

Flutter桌面版Json解析工具设计
3. PC端的装置进程。一般PC上的软件有两种方法能够装置,一个是绿色免装置包,复制进来直接就能用,一个是装置包,双击解压,装置到磁盘指定目录。

Flutter桌面版Json解析工具设计

上面是官网阐明,确实是支撑,不过现在本人还未尝试过。

参阅代码

完好的参阅代码在 github.com/18598925736…

有关Flutter PC端开发的问题欢迎留言评论。