Flutter如何将代码显示到界面上
前言
如何优雅的将项目中的代码,亦或是你的 demo 代码展示到界面上?本文对使用简单、便于维护且通用的解决方案,进行相关的对比和探究
为了节省大家的时间,把最终解决方案的相关接入和用法写在前面
预览代码
快速开始
接入:pub,github
dependencies:
code_preview: ^0.1.5
用法:CodePreview,提供需要预览的 className,可自动匹配该类对应的代码文件
本来想把写法简化成传入对象,但是因为一些原因无奈放弃,改成了
className
具体可以参考下面
Flutter Web中的问题
模块的说明
import 'package:code_preview/code_preview.dart';
import 'package:flutter/material.dart';
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const CodePreview(className: 'Test');
}
}
使用效果:flutter_smart_dialog
配置代码文件
因为原理是遍历资源文件,所以必须将需要展示的代码文件或者其文件夹路径,定义在 assets 下,这步操作,为大家提供了一个自动化的插件解决
强烈建议需要展示到界面的代码,都放在统一的文件夹里管理
展示界面的代码需要在 pugspec.yaml 中的 assets 定义
如果代码预览的文件夹,分级复杂,每次都需要定义路径实在麻烦
提供一个插件:Flutter Code Helper
安装:Plugins 中搜索
Flutter Code Helper
pugspec.yaml 中定义下需要自动生成文件夹的路径,文件夹随便套娃,会自动帮你递归在 assets 下生成
不需要自动生成,可:不写该配置,或者配置空数组(auto_folder: [])
code_helper:
auto_folder: [ "assets/", "lib/widgets/" ]
说明下:上面的插件是基于 RayC 的 FlutterAssetsGenerator 插件项目改的
看了下 RayC 的插件代码和相关功能,和我预想的上面功能实现有一定出入,改动起来变动较大
想试下插件项目的各种新配置,直接拉到最新
后期如果想到需要什么功能,方便随时添加
所以没向其插件里面提 pr,就单独新开了个插件项目
高级使用
主题
提供俩种代码样式主题
日间模式
CodePreview.config = CodePreviewConfig(codeTheme: CodeTheme.light);
夜间模式
CodePreview.config = CodePreviewConfig(codeTheme: CodeTheme.dark);
注释解析
你可以使用如下的格式,在类上添加注释
key 的前面必须加
@
,举例(@title,@xxx)key 与 value 的之间,必须使用
分号
分割,举例(@xxx: xxx)value 如果需要换行,换行的文案前必须加
中划线
/// @title:
/// - test title one
/// - test title two
/// @content: test content
/// @description: test description
class OneWidget extends StatelessWidget {
const OneWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
然后可以从
customBuilder
的回调获取 param 参数,param 中拥有 parseParam 参数获取取得上面注释的数据:param.parseParam ['title'] 或者 param.parseParam ['***']
获取的 value 的类型是 List<String>,可兼容多行 value 的类型
customBuilder
的用法codeWidget
内置的代码预览布局,如果你想定义自己预览代码的布局,那就可以不使用codeWidget
一般来说,可以根据注释获取的数据,结合
codeWidget
嵌套来自定义符合要求的布局param
中含有多个有用内容,可自行查看
import 'package:code_preview/code_preview.dart';
import 'package:flutter/material.dart';
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CodePreview(
className: 'OneWidget',
customBuilder: (Widget codeWidget, CustomParam? param) {
debugPrint(param?.parseParam['title'].toString());
debugPrint(param?.parseParam['content'].toString());
debugPrint(param?.parseParam['description'].toString());
return codeWidget;
},
);
}
}
目前内部预览的布局,会自动去掉类上的注释,如果想保留注释,可自行匹配下
CodePreview.config = CodePreviewConfig(removeParseComment: false);
几种代码预览方案
FlutterUnit 方案
https://github.com/toly1994328/FlutterUnit
FlutterUnit 项目也是自带代码预览方案,这套方案是比较特殊方案
大概看了下,整个 FlutterUnit 的数据都是基于
flutter.db
,该文件里面就有相关 demo 的文本信息所有的 demo 也是单独存在一个叫
widgets
的项目中所以大概可以猜测出
应该会有个 db 的辅助工具,会去扫描
widgets
的项目中的 demo 代码将他们的文本信息都扫描出来,然后解析上面的注释等相关信息,分类存储到数据库中,最后生成 db 文件
映射表,宿主可以通过 db 中的组件类名,从这里拿到 demo 效果实例
总结
整套流程看下来,实现起来的工作量还是有点大的
db 辅助工具的编写
文本注释相关解析规则
如何便捷的维护 db 文件(辅助工具是否支持,生成后自动覆盖宿主 db 文件)
不同平台 db 文件的读取和相关适配
优点
因为扫描工具不依赖 Flutter 相关库,预览方案可以快速的移植到其它编程语言(compose,SwiftUI 等)
具备高度自定义,因为是完全独立的第三方扫描工具,可以随性所欲的定制化
缺点
最明显的缺点,应该就是稍微改下 demo 代码,就需要三方工具重新生成 db 文件(如果三方工具实现的是 cli 工具,可以将扫描生成命令和 push 等命令集成一起,应该可以比较好的避免该问题)
build_runner 方案
https://pub.dev/packages/build_runner
build_runner 是个强大代码自动生成工具,根据 ast 语法树 + 自定义注解信息,可以生成很多强大的附属代码信息,例如 json_serializable
等库
所以,也能利用这点自定义类注解,获取到对应的整个类的代码信息,在对应附属的 xx.g.dart
文件中,将获取的代码内容转换成字符串,然后直接将 xx.g.dart
文件的代码字符串信息,展示到界面就行了
优点
可以通过生成命令,全自动的生成代码,甚至将整个预览 demo 的映射表都可以自动配置完成
可以规范的通过注解配置多个参数
缺点
因为
build_runner
需要解析整个 ast 语法树,一旦项目很大之后,解析生成的时间会非常非常的长!因为现在很多的这类库都是依赖
build_runner
,所以跑自动生成命令,会导致巨多xx.g.dart
文件被改动,极大的增加 cr 工作量
资源文件方案
这应该最常用的一种方案
在
pubspec.yaml
中的assets
中定义下我们代码文件路径
flutter:
assets:
- lib/widgets/show/
然后用 loadString 获取文件内容
final code = await rootBundle.loadString('lib/widgets/show/custome_dialog_animation.dart');
优点
侵入性非常低,不会像
build_runnner
方案那样影响到其它模块便于维护,如果 demo 预览代码被改变了,打包的时候,资源文件也会生成对应改变后的代码文件
缺点
使用麻烦,使用的时候需要传入具体的文件路径,才能找到想要的代码资源文件
需要反复的在
pubspec.yaml
中的assets
里面定义文件路径
资源文件方案优化
上面的三种方案各有优缺点,明确当前的诉求
目前是想写个简单的,通用的,仅在 Flutter 中实现代码预览方案
要求使用简单,高效
维护简单,多人开发的时候不会有很大成本
FlutterUnit 方案:实现起来成本较大,且多人开发对单个 db 文件的维护很可能会有点问题,例如:更新代码的时候,db 文件忘记更新
build_runner 方案:生成时间是个问题,还有很对其他类型 xx.g.dart
文件产生影响也比较麻烦
资源文件方案:整体是符合预期的,但是使用时候,需要传入路径和 pubspec.yaml
中反复定义文件路径,这是俩个很大痛点
结合实现成本和诉求,选择资源文件方案
,下面对其痛点进行优化
使用优化
Flutter 的编译产物中,有个相当有用的文件:AssetManifest.json
AssetManifest.json 文件里面,有所有的资源文件的路径,然后就简单了,我们只需要读取该文件内容
final manifestContent = await rootBundle.loadString('AssetManifest.json');
获取到所有的路径之后,再结合传入的类名,读取所有路径的文件内容,然后和传入的类名做正则匹配就行了
稍微优化
将传入的类名,转换为下划线名称和所有路径名称做匹配,如果能匹配上,再进行内容匹配,匹配成功后就返回该文件的代码内容
如果上述匹配失败,就进行兜底的全量匹配
优化前
import 'package:code_preview/code_preview.dart';
import 'package:flutter/material.dart';
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const CodePreview(path: 'lib/widgets/show/custome_dialog_animation.dart');
}
}
优化后
import 'package:code_preview/code_preview.dart';
import 'package:flutter/material.dart';
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const CodePreview(className: 'CustomDialogAnimation');
}
}
一般来说,我是统一配置预览 demo 和 className,这样比较好对照
路径定义优化
本来是想在 pubspec.yaml
的 assets
里面直接写通配符定义全路径,然后悲剧了,它不支持这种写法
flutter:
assets:
- lib/widgets/**/*.dart
GG,只能想其他办法了,想了很多方法都不行,只能从外部入手,用 idea 插件的形式,实现自动化扫描生成路径
安装:Plugins 中搜索
Flutter Code Helper
pugspec.yaml 中定义下需要自动生成文件夹的目录,文件夹随便套娃,会自动帮你递归在 assets 下生成
不需要自动生成,可:不写该配置,或者配置空数组(auto_folder: [])
code_helper:
auto_folder: [ "assets/", "lib/widgets/" ]
Flutter Web 中的问题
魔幻的 runtimeType
flutter web 的 release 模式中
dart2js 会压缩 JS,这样会使得类型名被改变
例如:dart 中的
TestWidgetFunction
类的 runtimeType,可能会变成minified:Ah
,而不是TestWidgetFunction
!
为啥需要压缩呢?压缩名称可以使得编译器将 JavaScript 体积缩小 3 倍 +;精确等效语义和性能 / 代码大小之间的权衡,Dart 明显是选择了后者
这种情况只会在 Flutter Web 的 release 模式下发生,其他平台和 Flutter web 的 Debug | Profile 模式都不会有这种问题;所以说 Xxx.runtimeType.toString
,并不一定会得到预期内的数据。。。
具体讨论可参考
https://github.com/dart-lang/sdk/issues/35052
https://github.com/flutter/flutter/issues/78041
解决思路
将压缩类型
minified:Ah
恢复成Test
将获取的
Test
字符串使用相同算法压缩成minified:Ah
如有知道如何实现的,务必告诉鄙人
下面从压缩级别调整的角度,探究是否可解决该问题
dart2js 压缩说明
注:flutter build web 默认的是 O4 优化级别
O0: 禁用许多优化。
O1: 启用默认优化 (仅是 dart2js 该命令的默认级别)
O2: 在 O1 优化基础上,尊重语言语义且对所有程序安全的其他优化(例如缩小)
备注:使用 - O2, 使用开发 JavaScript 编译器编译时,类型的字符串表示不再与 Dart VM 中的字符串表示相同
O3: 在 O2 优化基础上,并省略隐式类型检查。
注意:省略类型检查可能会导致应用程序因类型错误而崩溃
O4: 在 O3 优化基础上,启用更积极的优化
注意:O4 优化容易受到输入数据变化的影响,在依赖 O4 之前,需测试用户输入中的边缘情况
下面是 flutter 新建项目,未做任何改动,不同压缩级别的 js 产物体积
# main.dart.js: 7.379MB
flutter build web --dart2js-optimization O0
# main.dart.js: 5.073MB
flutter build web --dart2js-optimization O1
# main.dart.js: 1.776MB
flutter build web --dart2js-optimization O2
# main.dart.js: 1.716MB
flutter build web --dart2js-optimization O3
# main.dart.js: 1.687MB
flutter build web --dart2js-optimization O4
总结
预期用法
为什么想使用对象?因为当对象名称改变时,对应使用的地方,可以便捷观察到需要改变
可以使用传入的对象实例,在内部使用 runtimeType 获取类型名,再进行相关匹配
CodePreview(code: Test());
但是
综上可知,使用 flutter build web --dart2js-optimization O1
编译的 flutter web release 产物,能够使得 runtimeType 的语义和 Dart VM 中字符串保持一致
但是该压缩级别下的,js 体积过于夸张,务必会对加载速度产生极大影响,可想而知,在复杂项目中的体积增涨肯定更加离谱
对于想要用法更加简单,使用低级别压缩命令打包的想法需要舍弃
用法不得已做妥协
CodePreview(className: "Test");
这是个让我非常纠结的思路历程
最后
到这里也结束了,自我感觉,对大家应该能有一些帮助
一般来说,大部分团队,都会有个自己的内部组件库,因为 Flutter 强大的跨平台特性,所以就能很轻松的发布到 web 平台,可以方便的体验各种组件的效果,结合文章中的代码预览方案,就可以更加快速的上手各种组件用法了~
END
🌟 活动推荐
2023 年 5 月 27-28 日,GOTC 2023 全球开源技术峰会将在上海张江科学会堂隆重举行。
为期 2 天的开源行业盛会,将以行业展览、主题发言、特别论坛、分论坛、快闪演讲的形式来诠释此次大会主题 ——“Open Source, Into the Future”。与会者将一起探讨元宇宙、3D 与游戏、eBPF、Web3.0、区块链等热门技术主题,以及 OSPO、汽车软件、AIGC、开源教育培训、云原生、信创等热门话题,探讨开源未来,助力开源发展。
长按识别下方二维码立即查看 GOTC 2023 详情/报名。
微信扫码关注该文公众号作者