“我正在参加「掘金·启航计划」”
前言
Flutter
从一出生就跟隔壁的 facebook/react-native: A framework for building native applications using React (github.com)进行对比。不能热更新,一直都是大家选型的时候顾虑。这个问题也在
Code Push / Hot Update / out of band updates · Issue #14330 · flutter/flutter (github.com) 讨论的非常激烈。
Fair
不废话,这里就直接上主题了,选择使用 wuba/Fair: A Flutter package used to update widget tree dynamically. Fair提供一整套Flutter动态化解决方案 (github.com) 的原因主要是下面几点:
- 社区比较活跃,维护着最新的
Flutter SDK
。介绍的相关文章比较多。 - 组件映射 +
js
的逻辑,Fair
布局使用DSL
现实元数据驱动构建,逻辑同样使用自定义转化后的js
处理,Fair
只有基本的逻辑处理才会使用JSCore
来完成。 - 对现存的代码的改动比较小 ,可以同时支持
Flutter
aot
以及Fair
下发热更新的需求。 - 有一套开源的热更新服务平台。
文章后续内容都是基于 wuba/Fair at fair-release-zmtzawqlp (github.com) 分支。
准备环境
Flatbuffers
Fair
的产物中组件映射是一个 json
,但是我们可以选择生成 .bin
,它是由 google/flatbuffers: FlatBuffers: Memory Efficient Serialization Library (github.com)(解码速度极快,内存使用效率高) 生成。flatbuffers/dart 来提供解析。
我们将 Flatbuffers
官方打包好的产物下载下来。
并且在环境变量中进行配置。
-
windows
-
mac
配置好之后,使用 Fair
就会生成对应的 bin
文件。
版本
fair_version
主要是对 flutter sdk
中的组件做了映射,最新版本支持 flutter
的 stable
分支 3.7.0
。
# add Fair dependency
dependencies:
fair: 3.2.1
# add build_runner and compiler dependency
dev_dependencies:
build_runner: ^2.0.0
fair_compiler: ^1.7.0
# switch "fair_version" according to the local Flutter SDK version
# Flutter SDK 3.7.x(3.7.0、3.7.1、3.7.2、3.7.3、3.7.4、3.7.5、3.7.6、3.7.7、3.7.8、3.7.9、3.7.10) -> flutter_3_7_0
# Flutter SDK 3.3.x(3.3.0、3.3.1、3.3.2、3.3.3、3.3.4、3.3.5、3.3.6、3.3.7、3.3.8、3.3.9、3.3.10) -> flutter_3_3_0
# Flutter SDK 3.0.x(3.0.0、3.0.1、3.0.2、3.0.3、3.0.4、3.0.5) -> flutter_3_0_0
# Flutter SDK 2.10.x(2.10.0、2.10.1、2.10.2、2.10.3) -> flutter_2_10_0
# Flutter SDK 2.8.x(2.8.0、2.8.1) -> flutter_2_8_0
# Flutter SDK 2.5.x(2.5.0、2.5.1、2.5.2、2.5.3) -> flutter_2_5_0
# Flutter SDK 2.0.6 -> flutter_2_0_6
# Flutter SDK 1.22.6 -> flutter_1_22_6
dependency_overrides:
fair_version: 3.7.0
入口
入口变化:
void main() {
runApp(const MyApp());
}
改为
void main() {
WidgetsFlutterBinding.ensureInitialized();
FairApp.runApplication(
FairApp(child: const MyApp()),
);
}
生成产物
将需要热更新的页面用 FairPatch
进行标记,一个文件里面只能有一个。
import 'package:fair/fair.dart';
import 'package:flutter/material.dart';
@FairPatch()
class TestPage extends StatefulWidget {
const TestPage({super.key});
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(child: Text('TestPage')),
);
}
}
然后再执行命令 flutter pub run build_runner build --delete-conflicting-outputs
,在 build/fair
文件夹里面就会生成对应的产物。
bin/json
文件是页面组件对应的映射js
是页面逻辑metadata
是这次生成产物的一些信息
# Generated by Fair on 2023-05-02 23:58:55.851520.
source: fair_gallery|lib/src/page/simple/test.dart
md5: 5ad455ea918a23da4fe99cec43dada3d
json: fair_gallery|build/fair/lib_src_page_simple_test.fair.json
bin: fair_gallery|build/fair/lib_src_page_simple_test.fair.bin
date: 2023-05-02 23:58:55.851520
fair_patch.zip
是产物的 zip 包
使用
指定对应的资源就行了,这里可以是 asset
资源,也可以是网络资源。需要注意的时候默认读取资源 bin/json
的时候,也会根据相同的路径,去读取对应的 js
资源。
- 比如
xxxx.bin
对应xxxx.js
- 比如
xxxx.json
对应xxxx.js
FairWidget(
path: 'xxxx.bin',
name: '页面的名字',
// 入参
data: <String, dynamic>{},
// 加载资源时候的占位
holder: (context) {
return Container();
},
);
原理
简单的来说,Fair
将 build
方法体里面的组件映射成了 json
格式;对于 build
方法体之外的部分,转换成了 js
,以支持动态逻辑。
build 方法体内
示例
import 'package:fair/fair.dart';
import 'package:flutter/material.dart';
@FairPatch()
class TestPage extends StatefulWidget {
const TestPage({super.key});
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(child: Text('TestPage')),
);
}
}
生成对应的 json
文件, 这就是我们这个页面组件部分的映射。
{
"className": "Scaffold",
"na": {
"appBar": {
"className": "AppBar"
},
"body": {
"className": "Center",
"na": {
"child": {
"className": "Text",
"pa": [
"TestPage"
]
}
}
}
},
"methodMap": {}
}
className
对应这个组件的类名,对应到组件映射里面。na
代表命名参数(named arguments),pa
代表位置参数(positional arguments)。
这部分是由 Fair/dart2dsl at main · wuba/Fair (github.com) 利用 analyzer | Dart Package (flutter-io.cn) 生成的。之前的 Dart 怎么阻止你的同事使用Getx(自定义Lint) – 掘金 (juejin.cn) 和 Flutter 法法路由 10.0 – 掘金 (juejin.cn) 中有介绍,这里就不展开说了。
FairDelegate
由于生成的都是 json
组件映射,那么一些特殊的类,比如 ScrollController
我们是没法直接表达,只能通过 FairDelegate
去绑定。
import 'package:fair/fair.dart';
import 'package:flutter/material.dart';
@FairPatch()
class TestPage extends StatefulWidget {
const TestPage({super.key});
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
late ScrollController _controller;
@override
void initState() {
super.initState();
onLoad();
}
void onLoad() {
_controller = ScrollController();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: SingleChildScrollView(
controller: _controller,
child: const Center(
child: Text('TestPage'),
),
),
);
}
}
生成的 json
如下:
{
"className": "Scaffold",
"na": {
"appBar": {
"className": "AppBar"
},
"body": {
"className": "SingleChildScrollView",
"na": {
"controller": "^(_controller)",
"child": {
"className": "Center",
"na": {
"child": {
"className": "Text",
"pa": [
"TestPage"
]
}
}
}
}
}
},
"methodMap": {}
}
这个 _controller
,Fair
会去哪里找呢?
自定义 FairDelegate
我们需要自定义个 FairDelegate
,比如 TestPageFairDelegate
。
class TestPageFairDelegate extends FairDelegate {
late ScrollController _controller;
@override
void initState() {
super.initState();
onLoad();
}
void onLoad() {
_controller = ScrollController();
}
@override
Map<String, PropertyValue> bindValue() {
return {
...super.bindValue(),
// key 跟页面上面的名字一致
'_controller': () => _controller,
};
}
@override
Map<String, Function> bindFunction() {
return super.bindFunction();
}
}
注册 TestPageFairDelegate
然后我们需要去注册一下,让页面跟 TestPageFairDelegate
对应起来。比如这里页面的名字定义为 TestPage
。
FairWidget(
path: 'xxxx.bin',
name: 'TestPage',
);
FairApp(
child: const MyApp(),
delegate: <String, FairDelegateBuilder>{
'TestPage': (context, data) => TestPageFairDelegate(),
},
);
这样子,当通过映射去组装组件的时候,就会尝试去 TestPageFairDelegate
中去寻找 _controller
。当一些方法不方便在 build
方法体之外处理(因为这部分会转换成 js
),我们也能通过这种方法来绑定方法。
class TestPageFairDelegate extends FairDelegate {
@override
Map<String, Function> bindFunction() {
return {
...super.bindFunction(),
'_onRefresh': _onRefresh,
};
}
void _onRefresh() {}
}
build 方法体外
方法体之外的部分,会生成 js
,以满足逻辑的动态化。
import 'package:fair/fair.dart';
import 'package:flutter/material.dart';
@FairPatch()
class TestPage extends StatefulWidget {
const TestPage({super.key});
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(
child: Text('TestPage'),
),
);
}
}
生成的 js
(已格式化)如下,
它是由 Fair/dart2js at main · wuba/Fair (github.com) 利用 analyzer | Dart Package (flutter-io.cn) 生成的。
GLOBAL["#FairKey#"] = (function (__initProps__) {
const __global__ = this;
return runCallback(function (__mod__) {
with (__mod__.imports) {
function _TestPageState() {
const inner = _TestPageState.__inner__;
if (this == __global__) {
return new _TestPageState({ __args__: arguments });
} else {
const args =
arguments.length > 0 ? arguments[0].__args__ || arguments : [];
inner.apply(this, args);
_TestPageState.prototype.ctor.apply(this, args);
return this;
}
}
_TestPageState.__inner__ = function inner() {};
_TestPageState.prototype = {};
_TestPageState.prototype.ctor = function () {};
return _TestPageState();
}
}, []);
})(convertObjectLiteralToSetOrMap(JSON.parse("#FairProps#")));
实际上一个完整生命周期的页面如下:
import 'package:fair/fair.dart';
import 'package:flutter/material.dart';
@FairPatch()
class TestPage extends StatefulWidget {
const TestPage({super.key, this.fairProps});
final dynamic fairProps;
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
@FairProps()
var fairProps;
@override
void initState() {
super.initState();
fairProps = widget.fairProps;
onLoad();
}
/// void didChangeDependencies() {
/// runtime?.invokeMethod(pageName, 'onLoad', null);
/// }
void onLoad() {}
/// void dispose() {
/// runtime?.invokeMethod(pageName, 'onUnload', null);
/// }
///
void onUnload() {}
@override
void dispose() {
onUnload();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(
child: Text('TestPage'),
),
);
}
}
对应的 js
(已格式化) 如下:
GLOBAL["#FairKey#"] = (function (__initProps__) {
const __global__ = this;
return runCallback(function (__mod__) {
with (__mod__.imports) {
function _TestPageState() {
const inner = _TestPageState.__inner__;
if (this == __global__) {
return new _TestPageState({ __args__: arguments });
} else {
const args =
arguments.length > 0 ? arguments[0].__args__ || arguments : [];
inner.apply(this, args);
_TestPageState.prototype.ctor.apply(this, args);
return this;
}
}
_TestPageState.__inner__ = function inner() {
this.fairProps = __initProps__;
};
_TestPageState.prototype = {
initState: function initState() {
const __thiz__ = this;
with (__thiz__) {
fairProps = widget.fairProps;
onLoad();
}
},
onLoad: function onLoad() {
const __thiz__ = this;
with (__thiz__) {
}
},
onUnload: function onUnload() {
const __thiz__ = this;
with (__thiz__) {
}
},
dispose: function dispose() {
const __thiz__ = this;
with (__thiz__) {
onUnload();
}
},
};
_TestPageState.prototype.ctor = function () {};
return _TestPageState();
}
}, []);
})(convertObjectLiteralToSetOrMap(JSON.parse("#FairProps#")));
-
onLoad
会在FairWidget
的didChangeDependencies
方法执行的时候通过BasicMessageChannel
去通知到js
当中的onLoad
方法。 -
onUnload
会在FairWidget
的dispose
方法执行的时候通过BasicMessageChannel
去通知到js
当中的onUnload
方法。
通过上面的代码,我们同一个页面可以同时支持 Flutter
aot
和 js
的逻辑。
IFairPlugin
通过上面所知,build
方法体外面的代码是需要能被转换成 js
的才行。这明显不能满足我们需要跟 Flutter
api
交互的需求。
为此,Fair
提供了 js
跟 Flutter
进行交互的方法。
FairCommonPlugin
首先,我们需要定义一个单例 FairCommonPlugin
,名字必须是这个。
class FairCommonPlugin extends IFairPlugin {
factory FairCommonPlugin() => _fairCommonPlugin;
FairCommonPlugin._();
static final FairCommonPlugin _fairCommonPlugin = FairCommonPlugin._();
@override
Map<String, Function> getRegisterMethods() {
return {};
}
}
接着,定义一个方法,比如 http
, 用于网络请求。这里你可以直接把 http
方法写在 FairCommonPlugin
中,但是我更推荐使用 mixin
将功能分离开。
mixin HttpPlugin implements FairCommonPluginMixin {
Future<dynamic> http(dynamic map) => request(
map,
(dynamic requestMap) async {
final method = requestMap['method'];
final url = requestMap['url'];
final headers = requestMap['headers'];
final body = requestMap['body'];
switch (method) {
case 'GET':
final Response result =
await HttpClientHelper.get(Uri.parse(url)) as Response;
return {
'json': jsonDecode(result.body),
'statusCode': result.statusCode,
};
case 'POST':
final Response result = await HttpClientHelper.post(
Uri.parse(url),
headers: headers,
body: body,
) as Response;
return {
'json': jsonDecode(result.body),
'statusCode': result.statusCode,
};
default:
}
return null;
},
);
}
接着,将 HttpPlugin
加到 FairCommonPlugin
中 。
class FairCommonPlugin extends IFairPlugin with HttpPlugin {
factory FairCommonPlugin() => _fairCommonPlugin;
FairCommonPlugin._();
static final FairCommonPlugin _fairCommonPlugin = FairCommonPlugin._();
@override
Map<String, Function> getRegisterMethods() {
return {
'http': http,
};
}
}
定义 js
你需要你项目的 asset
下面增加 fair_basic_config.json
,内容如下:
{
"plugin": {
"fair_common_plugin": "assets/plugin/fair_common_plugin.js"
}
}
接着定义我们的 fair_common_plugin.js
文件。注意,这里的 js
中的 FairCommonPlugin
与我们的 dart
里面定义的 FairCommonPlugin
名字一致,然后就是 js
中的 http
也与我们刚才定义的方法名一致。
let FairCommonPlugin = function () {
return {
http: function (resp) {
fairCommonPluginRequest(resp, 'http');
}
}
}
使用
我们增加在 onLoad
方法中去请求接口。注意,当中的 pageName
和 callback
是固定的名称,pageName
是必填,callback
则是根据你场景,可以不填。其他参数根据自己场景,自己进行约定。
import 'dart:convert';
import 'package:fair/fair.dart';
import 'package:flutter/material.dart';
import 'package:http_client_helper/http_client_helper.dart';
@FairPatch()
class TestPage extends StatefulWidget {
const TestPage({super.key, this.fairProps});
final dynamic fairProps;
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
@FairProps()
var fairProps;
final String _pageName = '#FairKey#';
var feedList = [];
@override
void initState() {
super.initState();
fairProps = widget.fairProps;
onLoad();
}
void onLoad() {
FairCommonPlugin().http({
'method': 'GET',
'url': 'https://api.tuchong.com/feed-app',
// required
'pageName': _pageName,
// if need, add a callback
'callback': (dynamic result) {
if (result != null) {
var statusCode = result['statusCode'];
if (statusCode == 200) {
var map = result['json'];
if (map != null) {
feedList = map['feedList'];
setState(() {});
}
}
}
},
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(
child: Text('TestPage'),
),
);
}
}
对应的 js
(已格式化) 如下:
GLOBAL["#FairKey#"] = (function (__initProps__) {
const __global__ = this;
return runCallback(function (__mod__) {
with (__mod__.imports) {
function _TestPageState() {
const inner = _TestPageState.__inner__;
if (this == __global__) {
return new _TestPageState({ __args__: arguments });
} else {
const args =
arguments.length > 0 ? arguments[0].__args__ || arguments : [];
inner.apply(this, args);
_TestPageState.prototype.ctor.apply(this, args);
return this;
}
}
_TestPageState.__inner__ = function inner() {
this.fairProps = __initProps__;
this._pageName = "#FairKey#";
this.feedList = [];
};
_TestPageState.prototype = {
initState: function initState() {
const __thiz__ = this;
with (__thiz__) {
fairProps = widget.fairProps;
onLoad();
}
},
onLoad: function onLoad() {
const __thiz__ = this;
with (__thiz__) {
FairCommonPlugin().http(
convertObjectLiteralToSetOrMap({
["method"]: "GET",
["url"]: "https://api.tuchong.com/feed-app",
["pageName"]: _pageName,
["callback"]: function dummy(result) {
if (result != null) {
let statusCode = result.__op_idx__("statusCode");
if (statusCode == 200) {
let map = result.__op_idx__("json");
if (map != null) {
feedList = map.__op_idx__("feedList");
setState("#FairKey#", function dummy() {});
}
}
}
},
})
);
}
},
};
_TestPageState.prototype.ctor = function () {};
return _TestPageState();
}
}, []);
})(convertObjectLiteralToSetOrMap(JSON.parse("#FairProps#")));
至此,我们完成了在 js 中跟 Flutter 交互。更多的交互,只需要在 js
中 和 dart
中定义对应的方法即可。
生成组件映射
Fair
当中自带的时候 Flutter sdk
中的一些常用映射,当我们需要使用到一些三方库,或者自己的写的组件的时候,应该怎么办?
AppGeneratedModule
Fair
提供了 FairBinding
注解,来生成相应的映射。你可以使用到的三方库或者该项目中的组件,添加到 FairBinding
当中。
@FairBinding(
packages: ['package:extended_image/extended_image.dart'],
)
void main() {
}
执行命令 flutter pub run build_runner build --delete-conflicting-outputs
,会在 lib/src/
下面生成一个 generated.fair.dart
文件。下面代码只是一部分。
class AppGeneratedModule extends GeneratedModule {
@override
Map<String, dynamic> components() {
return {
'ExtendedImage': (props) => ExtendedImage(
key: props['key'],
image: props['image'],
semanticLabel: props['semanticLabel'],
excludeFromSemantics: props['excludeFromSemantics'] ?? false,
width: props['width']?.toDouble(),
height: props['height']?.toDouble(),
color: props['color'],
opacity: props['opacity'],
colorBlendMode: props['colorBlendMode'],
fit: props['fit'],
alignment: props['alignment'] ?? Alignment.center,
repeat: props['repeat'] ?? ImageRepeat.noRepeat,
centerSlice: props['centerSlice'],
matchTextDirection: props['matchTextDirection'] ?? false,
gaplessPlayback: props['gaplessPlayback'] ?? false,
filterQuality: props['filterQuality'] ?? FilterQuality.low,
loadStateChanged: props['loadStateChanged'],
border: props['border'],
shape: props['shape'],
borderRadius: props['borderRadius'],
clipBehavior: props['clipBehavior'] ?? Clip.antiAlias,
enableLoadState: props['enableLoadState'] ?? false,
beforePaintImage: props['beforePaintImage'],
afterPaintImage: props['afterPaintImage'],
mode: props['mode'] ?? ExtendedImageMode.none,
enableMemoryCache: props['enableMemoryCache'] ?? true,
clearMemoryCacheIfFailed: props['clearMemoryCacheIfFailed'] ?? true,
onDoubleTap: props['onDoubleTap'],
initGestureConfigHandler: props['initGestureConfigHandler'],
enableSlideOutPage: props['enableSlideOutPage'] ?? false,
constraints: props['constraints'],
extendedImageEditorKey: props['extendedImageEditorKey'],
initEditorConfigHandler: props['initEditorConfigHandler'],
heroBuilderForSlidingPage: props['heroBuilderForSlidingPage'],
clearMemoryCacheWhenDispose:
props['clearMemoryCacheWhenDispose'] ?? false,
extendedImageGestureKey: props['extendedImageGestureKey'],
isAntiAlias: props['isAntiAlias'] ?? false,
handleLoadingProgress: props['handleLoadingProgress'] ?? false,
layoutInsets: props['layoutInsets'] ?? EdgeInsets.zero,
),
'CropAspectRatios.custom': CropAspectRatios.custom,
'CropAspectRatios.original': CropAspectRatios.original,
'CropAspectRatios.ratio1_1': CropAspectRatios.ratio1_1,
'CropAspectRatios.ratio3_4': CropAspectRatios.ratio3_4,
'CropAspectRatios.ratio4_3': CropAspectRatios.ratio4_3,
'CropAspectRatios.ratio9_16': CropAspectRatios.ratio9_16,
'CropAspectRatios.ratio16_9': CropAspectRatios.ratio16_9,
};
}
@override
Map<String, bool> mapping() {
return {
'ExtendedImage': true,
'CropAspectRatios.custom': false,
'CropAspectRatios.original': false,
'CropAspectRatios.ratio1_1': false,
'CropAspectRatios.ratio3_4': false,
'CropAspectRatios.ratio4_3': false,
'CropAspectRatios.ratio9_16': false,
'CropAspectRatios.ratio16_9': false,
};
}
}
- 在
components
方法中,ExtendedImage
对应其组件,CropAspectRatios.xxxx
对应了其静态值。 - 在
components
方法中,ExtendedImage
是true
,表明了它是一个组件(Widget)。CropAspectRatios.xxxx
则不是。
自定义自己的 AppGeneratedModule
有些时候,Fair
生成的映射会有缺失或者错误,定义一个自己的 AppGeneratedModule
并且继承 Fair
的AppGeneratedModule
,来满足我们自定的需求,并且不影响 Fair
生成的结果。
class MyAppGeneratedModule extends AppGeneratedModule {
@override
Map<String, dynamic> components() {
return <String, dynamic>{
...super.components(),
// add your cases here.
};
}
/// true means it's a widget.
@override
Map<String, bool> mapping() {
return <String, bool>{
...super.mapping(),
// remember add your cases here too.
};
}
}
设置 AppGeneratedModule
最后我们需要在 FairApp
中进行设置。
void main() {
WidgetsFlutterBinding.ensureInitialized();
FairApp.runApplication(
FairApp(
child: const MyApp(),
generated: MyAppGeneratedModule(),
),
);
}
FunctionDomain
之前的版本的 Fair
, 不支持回调写法,我们不得不将列表的 itemBuilder
通过 FairDelegate
绑定, 然后写成另一个组件。
class ListPage extends StatefulWidget {
const ListPage({super.key});
@override
State<ListPage> createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
@override
Widget build(BuildContext context) {
return ListView.builder(itemBuilder: _itemBuilder);
}
Widget _itemBuilder(BuildContext context, int index) {
return FairWidget(
name: 'ListItemPage',
);
}
}
class ListPageDelegate extends FairDelegate {
@override
Map<String, Function> bindFunction() {
return {
...super.bindFunction(),
'_itemBuilder': _itemBuilder,
};
}
Widget _itemBuilder(BuildContext context, int index) {
return FairWidget(
name: 'ListItemPage',
);
}
}
FunctionDomain 是什么?
FunctionDomain
可以理解为方法的作用域。
import 'package:flutter/material.dart';
class ListPage extends StatefulWidget {
const ListPage({super.key});
@override
State<ListPage> createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
@override
Widget build(BuildContext context) {
return ListView.builder(itemBuilder: (b, index) {
return Center(
child: Text('$index'),
);
});
}
}
生成的 json
如下:
{
"className": "ListView.builder",
"na": {
"itemBuilder": {
"className": "FairFunction",
"body": {
"className": "Center",
"na": {
"child": {
"className": "Text",
"pa": [
"#($index)"
]
}
}
},
"parameters": {
"pa": [
"b",
"index"
]
},
"tag": "Widget Function(BuildContext, int)",
"rt": "Widget"
}
},
"methodMap": {}
}
现在会生成一个叫 FairFunction
的映射,其中
body
为该回调方法返回的内容parameters
为该回调方法的参数tag
为该回调方法的唯一标识rt(return type)
为该回调方法的返回类型
在将这个映射转换成对应的方法的时候,Fair
会根据 tag
,返回对应的回调。而 FunctionDomain
将参数传递过去,对 "#($index)"
进行匹配,这样就能得到回调中的 index
。
var name = map[tag];
if(name != 'FairFunction') {
throw Exception('这个不是一个 FairFunction 节点!');
}
var functionTag = FunctionDomain.getTag(map);
switch (functionTag) {
// typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index)
// package:flutter/src/widgets/framework.dart
case 'Widget Function(BuildContext, int)':
List functionPaParameters = FunctionDomain.pa(map);
Widget Function(BuildContext, int) builder = (p0, p1) {
return pa0Value(
FunctionDomain.getBody(map),
methodMap,
context,
FunctionDomain(
{functionPaParameters[0]: p0, functionPaParameters[1]: p1},
parent: domain,
),
);
};
return builder;
默认支持的 FunctionDomain
而当前版本 Fair
已内置支持了 Flutter
sdk
中一些常见的回调。
回调 | 所在位置 |
---|---|
无入参回调,int,double,bool,String,Widget,以及它们的空类型和List类型 | 比如 int Function() |
typedef WidgetBuilder = Widget Function(BuildContext context) | package:flutter/src/widgets/framework.dart |
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index) | package:flutter/src/widgets/framework.dart |
typedef NullableIndexedWidgetBuilder = Widget? Function(BuildContext context, int index) | package:flutter/src/widgets/framework.dart |
typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child) | package:flutter/src/widgets/framework.dart |
typedef GenerateAppTitle = String Function(BuildContext context) | package:flutter/src/widgets/app.dart |
typedef InputCounterWidgetBuilder = Widget? Function(BuildContext context, {required int currentLength, required int? maxLength, required bool isFocused}) | package:flutter/src/material/text_field.dart |
typedef ToolbarBuilder = Widget Function(BuildContext context, Widget child) | package:flutter/src/widgets/text_selection.dart |
typedef ReorderCallback = void Function(int oldIndex, int newIndex) | package:flutter/src/widgets/reorderable_list.dart |
typedef ReorderItemProxyDecorator = Widget Function(Widget child, int index, Animation animation) | package:flutter/src/widgets/reorderable_list.dart |
typedef ExpansionPanelCallback = void Function(int panelIndex, bool isExpanded) | package:flutter/src/material/expansion_panel.dart |
typedef ControlsWidgetBuilder = Widget Function(BuildContext context, ControlsDetails details) | package:flutter/src/material/stepper.dart |
typedef PopupMenuItemBuilder = List<PopupMenuEntry> Function(BuildContext context) | package:flutter/src/material/popup_menu.dart |
typedef DropdownButtonBuilder = List Function(BuildContext context) | package:flutter/src/material/dropdown.dart |
typedef ExpansionPanelHeaderBuilder = Widget Function(BuildContext context, bool isExpanded) | package:flutter/src/material/expansion_panel.dart |
typedef AnimatedCrossFadeBuilder = Widget Function(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) | package:flutter/src/widgets/animated_cross_fade.dart |
typedef ValueWidgetBuilder = Widget Function(BuildContext context, T value, Widget? child) | package:flutter/src/widgets/value_listenable_builder.dart |
typedef WillPopCallback = Future Function() | package:flutter/src/widgets/navigator.dart |
typedef StatefulWidgetBuilder = Widget Function(BuildContext context, void Function(void Function()) setState) | package:flutter/src/widgets/basic.dart |
typedef ImageFrameBuilder = Widget Function(BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) | package:flutter/src/widgets/image.dart |
typedef ImageLoadingBuilder = Widget Function(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) | package:flutter/src/widgets/image.dart |
typedef ImageErrorWidgetBuilder = Widget Function(BuildContext context, Object error, StackTrace? stackTrace) | package:flutter/src/widgets/image.dart |
typedef NestedScrollViewHeaderSliversBuilder = List Function(BuildContext context, bool innerBoxIsScrolled) | package:flutter/src/widgets/nested_scroll_view.dart |
typedef ChildIndexGetter = int? Function(Key key) | package:flutter/src/widgets/sliver.dart |
typedef ScrollableWidgetBuilder = Widget Function(BuildContext context, ScrollController scrollController) | package:flutter/src/widgets/draggable_scrollable_sheet.dart |
typedef LayoutWidgetBuilder = Widget Function(BuildContext context, BoxConstraints constraints) | package:flutter/src/widgets/layout_builder.dart |
typedef OrientationWidgetBuilder = Widget Function(BuildContext context, Orientation orientation) | package:flutter/src/widgets/orientation_builder.dart |
typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation animation) | package:flutter/src/widgets/animated_switcher.dart |
typedef AnimatedSwitcherLayoutBuilder = Widget Function(Widget? currentChild, List previousChildren) | package:flutter/src/widgets/animated_switcher.dart |
typedef AnimatedTransitionBuilder = Widget Function(BuildContext context, Animation animation, Widget? child) | package:flutter/src/widgets/dual_transition_builder.dart |
typedef HeroFlightShuttleBuilder = Widget Function(BuildContext flightContext, Animation animation, HeroFlightDirection flightDirection, BuildContext fromHeroContext, BuildContext toHeroContext) | package:flutter/src/widgets/heroes.dart |
typedef HeroPlaceholderBuilder = Widget Function(BuildContext context, Size heroSize, Widget child) | package:flutter/src/widgets/heroes.dart |
typedef AnimatedListItemBuilder = Widget Function(BuildContext context, int index, Animation animation) | package:flutter/src/widgets/animated_list.dart |
可以看到,已经包含了大部分的场景,如果三方或者自己定义的回调应该怎么办呢? 我们后面再讲。
js
通信
在组装组件的过程中,有一些参数是要从 js
获取的,大家都知道这个过程是一个同步操作,那么 Fair
是如何实现的?
final DynamicLibrary dl = Platform.isAndroid
? DynamicLibrary.open('libfairflutter.so')
: DynamicLibrary.open('FairDynamicFlutter.framework/FairDynamicFlutter');
Fair
利用了 ffi
(foreign function interface) 调用动态库,直接跟原生进行通信。
Pointer<Utf8> Function(Pointer<Utf8>) invokeJSCommonFuncSync = dl
.lookup<NativeFunction<Pointer<Utf8> Function(Pointer<Utf8>)>>(
'invokeJSCommonFuncSync')
.asFunction();
fair_gallery
前面我们简单地过了一下, Fair
的使用方法和原理。接下来通过 zmtzawqlp/fair_gallery: fair 🌰 (github.com) 展现更多使用过程中场景。
配置
使用的 Flutter
版本为 3.3.9
。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
fair:
git:
url: https://github.com/wuba/Fair.git
ref: fair-release-zmtzawqlp
#url: https://github.com/zmtzawqlp/fair.git
#ref: fixed
path: fair
dev_dependencies:
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
fair_compiler:
git:
url: https://github.com/wuba/Fair.git
ref: fair-release-zmtzawqlp
#url: https://github.com/zmtzawqlp/fair.git
#ref: fixed
path: compiler
dependency_overrides:
build_runner: 2.3.3
fair_version:
git:
url: https://github.com/wuba/Fair.git
ref: fair-release-zmtzawqlp
#url: https://github.com/zmtzawqlp/fair.git
#ref: fixed
path: flutter_version/flutter_mock
fair_dart2dsl:
git:
url: https://github.com/wuba/Fair.git
ref: fair-release-zmtzawqlp
#url: https://github.com/zmtzawqlp/fair.git
#ref: fixed
path: dart2dsl
fair_dart2js:
git:
url: https://github.com/wuba/Fair.git
ref: fair-release-zmtzawqlp
#url: https://github.com/zmtzawqlp/fair.git
#ref: fixed
path: dart2js
# 如果你是 3.3.0 以下的版本,需要 override collection
collection: 1.17.0
自定义资源加载
Fair
默认是支持 asset
和网络资源加载的。但我们在实际使用中,可以不只是简单的丢给它一个地址就行了。我们会去处理资源从哪里来,怎么保存。
JSDecoder
我们可以通过自定义 JSDecoder
,来控制 js
资源从哪里获取,需不需要缓存。
import 'dart:io';
import 'package:fair/src/internal/bundle_provider.dart';
import 'package:flutter/services.dart';
Map<String?, String> _cache = <String?, String>{};
/// 自定义 js 获取方式
class CustomFairJSDecoder with FairBundlePathCheck {
factory CustomFairJSDecoder() => _customFairJSDecoder;
CustomFairJSDecoder._();
static final CustomFairJSDecoder _customFairJSDecoder =
CustomFairJSDecoder._();
/// 自定义 js 获取方式
Future<String> decode(String? jsPath) async {
//
// var byteData = null;
// 根据自身的情况来,这个也可能从其他地方获取
//
// if (byteData != null) {
// return _cache[jsPath] ??= utf8.decode(byteData);
// }
return _cache[jsPath] ??= await _resolveAssert(jsPath);
}
Future<String> _resolveAssert(String? assertPath) async {
if (isExternalStoragePath(assertPath)) {
final file = File(assertPath ?? '');
return await file.readAsString();
}
return rootBundle.loadString(assertPath ?? '');
}
}
然后通过下面的方法,替换掉 Fair
默认的 FairJSDecoder
。
FairJSDecoder.resolve = (String? jsPath) async {
return CustomFairJSDecoder().decode(jsPath);
};
FairBundleProvider
我们可以自定义 FairBundleProvider
,来决定 bin/json
文件的来源,以及解析,决定是否要缓存解析结果。其中的 decode
的 isFlexBuffer
代表是 bin
文件还是 json
文件。
// ignore_for_file: implementation_imports
import 'dart:typed_data';
import 'package:fair/src/internal/fair_decoder.dart';
import 'package:fair/src/internal/bundle_provider.dart';
Map<String?, Map?> _cache = <String?, Map?>{};
class CustomFairBundleLoader extends FairBundleProvider {
@override
Future<Map?> onLoad(
String? path,
FairDecoder decoder, {
bool cache = true,
Map<String, String>? h,
}) async {
//
// var byteData = null;
// if (byteData != null) {
// //
// }
// else
// {
// 根据自身的情况来,这个也可能从其他地方获取
// 缓存的是 path 对应的 map
return _cache[path] ??= await super.onLoad(
path,
decoder,
cache: cache,
h: h,
);
}
//}
@override
Future<Map<dynamic, dynamic>> decode(
FairDecoder decoder, Uint8List data, bool isFlexBuffer) {
return super.decode(decoder, data, isFlexBuffer);
}
}
然后通过下面的方式设置,替换掉 Fair
默认的 BundleProvider
。
FairApp(
bundleProvider: CustomFairBundleLoader(),
),
自定义解析
前面在 FunctionDomain
提到 Fair
自带了一些映射,但是如果是三方库或者是自己写的回调方法呢?
DynamicWidgetBuilder
我们可以通过自定义 DynamicWidgetBuilder
,来达到自定解析过程的目的。
class CustomDynamicWidgetBuilder extends DynamicWidgetBuilder {
CustomDynamicWidgetBuilder(
super.proxyMirror,
super.page,
super.bound, {
super.bundle,
});
@override
dynamic convert(BuildContext context, Map map, Map? methodMap,
{Domain? domain}) {
var name = map[tag];
return super.convert(context, map, methodMap, domain: domain);
}
}
然后通过下面的方式设置,替换掉 Fair
默认的 DynamicWidgetBuilder
。
FairApp(
dynamicWidgetBuilder: (proxyMirror, page, bound, {bundle}) =>
CustomDynamicWidgetBuilder(proxyMirror, page, bound, bundle: bundle),
),
小栗子🌰
比如我们项目里面使用到了 loading_more_list | Flutter Package (flutter-io.cn)
示例
import 'package:fair/fair.dart';
import 'package:fair_gallery/src/utils/repository.dart';
import 'package:flutter/material.dart';
import 'package:loading_more_list/loading_more_list.dart';
@FairPatch()
class ListPage extends StatefulWidget {
const ListPage({super.key});
@override
State<ListPage> createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
@override
Widget build(BuildContext context) {
return LoadingMoreList(ListConfig(
itemBuilder: (context, item, index) {
return Text('$item');
},
// 忽略
sourceList: ...,
));
}
}
生成的 json
如下:
{
"className": "LoadingMoreList",
"pa": [
{
"className": "ListConfig",
"na": {
"itemBuilder": {
"className": "FairFunction",
"body": {
"className": "Text",
"pa": [
"#($item)"
]
},
"parameters": {
"pa": [
"context",
"item",
"index"
]
},
"tag": "Widget Function(BuildContext, dynamic, int)",
"rt": "Widget"
},
"sourceList": "^(_loadingMoreRepository)"
}
}
],
"methodMap": {}
}
可以看到 itemBuilder
生成了一个 FairFunction
。
body
为该回调方法返回的内容,是一个文本parameters
为该回调方法的参数,这里为啥需要生成参数呢? 因为参数(位置参数)的名字是可以随意改变的(命名参数的名字不可以改变)。tag
为该回调方法的唯一标识,这个就是我们用来做匹配的。rt(return type)
为该回调方法的返回类型。
定义自己的 DynamicWidgetBuilder
class CustomDynamicWidgetBuilder extends DynamicWidgetBuilder {
CustomDynamicWidgetBuilder(
super.proxyMirror,
super.page,
super.bound, {
super.bundle,
});
@override
dynamic convert(BuildContext context, Map map, Map? methodMap,
{Domain? domain}) {
// tag 就是 className
var name = map[tag];
switch (name) {
case 'FairFunction':
var tag = FunctionDomain.getTag(map);
switch (tag) {
// typedef LoadingMoreItemBuilder<in T> = Widget Function(BuildContext context, T item, int index)
// package:loading_more_list/src/list_config/loading_more_list_config.dart
case 'Widget Function(BuildContext, dynamic, int)':
List functionPaParameters = FunctionDomain.pa(map);
Widget Function(BuildContext, dynamic, int) builder = (p0, p1, p2) {
return pa0Value(
FunctionDomain.getBody(map),
methodMap,
context,
FunctionDomain(
{
functionPaParameters[0]: p0,
functionPaParameters[1]: p1,
functionPaParameters[2]: p2
},
parent: domain,
),
);
};
return builder;
}
break;
default:
}
return super.convert(context, map, methodMap, domain: domain);
}
}
-
dynamic convert(BuildContext context, Map map, Map? methodMap, {Domain? domain})
方法中map
就是我们生成的json
中的一个个Map
映射,通过className
,你就知道这个组件是什么东西。methodMap
为json
中生成的针对js
里面方法的映射。domain
是方法域,是父组件中的域。 -
我们对
FairFunction
里面的tag
Widget Function(BuildContext, dynamic, int)
进行筛选。
FunctionDomain
拿到用户定义的参数的名字(functionPaParameters
)以及对应名字的值(p0,p1,p2
), body
就会在该作用域下面,对相应的字符串(functionPaParameters
)进行匹配,替换成 (p0,p1,p2
)。
Widget Function(BuildContext, dynamic, int) builder = (p0, p1, p2) {
return pa0Value(
FunctionDomain.getBody(map),
methodMap,
context,
FunctionDomain(
{
functionPaParameters[0]: p0,
functionPaParameters[1]: p1,
functionPaParameters[2]: p2
},
parent: domain,
),
);
};
一个简单的自定义解析就写好了。
一些脚本
fair_gallery/bin at main · zmtzawqlp/fair_gallery (github.com) 在项目 bin
文件下,是一个有用的脚本
├─ bin
│ ├─ build.dart
│ ├─ dart_core.dart
│ ├─ extension.dart
│ ├─ fair_common_plugin.dart
│ ├─ generated_module
│ │ ├─ app.dart
│ │ ├─ binding.dart
│ │ ├─ flutter.dart
│ │ └─ packages.dart
│ └─ util
│ ├─ binding.dart
│ ├─ function_domain.dart
│ └─ utils.dart
生成自己的全量映射
Fair
自带的 fair_version
的映射生成是比较保守的,只生成了一部分,会导致可能你在写的时候会提示某个映射缺失。
生成 Flutter SDK 映射
├─ bin
│ ├─ generated_module
│ │ ├─ flutter.dart
执行该路径下的 flutter.dart
,会生成 flutter.bindings.dart
和 flutter.function.dart
文件。
├─ lib
│ └─ src
│ ├─ generated_module
│ │ ├─ flutter.bindings.dart
│ │ ├─ flutter.function.dart
flutter.bindings.dart
为你当前使用Flutter SDK
以及dart:ui
的全量映射。
flutter.function.dart
为flutter.bindings.dart
中涉及到回调方法的全量映射。
生成 三方库 映射
├─ bin
│ ├─ generated_module
│ │ ├─ binding.dart
│ │ ├─ packages.dart
执行该路径下的 packages.dart
,会读取 binding.dart
里面的三方引用。binding.dart
类似 FairBinding
,我没有用注解,是因为这样的话,有引用提示。
生成 packages.bindings.dart
和 packages.function.dart
文件。
├─ lib
│ └─ src
│ ├─ generated_module
│ │ ├─ packages.bindings.dart
│ │ ├─ packages.function.dart
packages.bindings.dart
为你当前使用三方库的全量映射。
packages.function.dart
为packages.bindings.dart
中涉及到回调方法的全量映射。
生成 本项目 映射
├─ bin
│ ├─ generated_module
│ │ ├─ binding.dart
│ │ ├─ app.dart
执行该路径下的 app.dart
,会读取 binding.dart
里面的本项目中的引用。binding.dart
类似 FairBinding
,我没有用注解,是因为这样的话,有引用提示。
生成 app.bindings.dart
和 app.function.dart
文件。
├─ lib
│ └─ src
│ ├─ generated_module
│ │ ├─ app.bindings.dart
│ │ ├─ app.function.dart
app.bindings.dart
为你当前使用项目中需要生成映射的文件的全量映射。
app.function.dart
为app.bindings.dart
中涉及到回调方法的全量映射。
使用
生成好这些映射之后,我们就要去使用它们了。
移除对 fair_version
中 Flutter SDK
的依赖
我们将不再依赖 fair_version
的对应的 Flutter SDK
版本,我们将引用 fair_version
的 flutter_mock
版本,这个是一个空的库。也就是说之后,Flutter SDK
将不再限制你。
dependency_overrides:
fair_version:
git:
url: https://github.com/wuba/Fair.git
ref: fair-release-zmtzawqlp
#url: https://github.com/zmtzawqlp/fair.git
#ref: fixed
path: flutter_version/flutter_mock
AppGeneratedModule
新的 AppGeneratedModule
将变成这样。
import 'package:fair/fair.dart';
import 'package:fair_gallery/src/generated_module/app.bindings.dart';
import 'package:fair_gallery/src/generated_module/flutter.bindings.dart';
import 'package:fair_gallery/src/generated_module/packages.bindings.dart';
class FairAppGeneratedModule extends GeneratedModule {
@override
Map<String, dynamic> components() {
return <String, dynamic>{
...appComponents,
...packagesComponents,
...flutterComponents,
// add your cases here.
};
}
/// true means it's a widget.
@override
Map<String, bool> mapping() {
return <String, bool>{
...appMapping,
...packagesMapping,
...flutterMapping,
// remember add your cases here too.
};
}
}
DynamicWidgetBuilder
新的 DynamicWidgetBuilder
将变成这样。
// ignore_for_file: implementation_imports, prefer_function_declarations_over_variables, void_checks
import 'package:fair_gallery/src/generated_module/app.function.dart';
import 'package:fair_gallery/src/generated_module/flutter.function.dart';
import 'package:fair_gallery/src/generated_module/packages.function.dart';
import 'package:flutter/material.dart';
import 'package:fair/fair.dart';
import 'package:fair/src/extension.dart';
class CustomDynamicWidgetBuilder1 extends DynamicWidgetBuilder
with
AppFunctionDynamicWidgetBuilder,
PackagesFunctionDynamicWidgetBuilder,
FlutterFunctionDynamicWidgetBuilder {
CustomDynamicWidgetBuilder1(
super.proxyMirror,
super.page,
super.bound, {
super.bundle,
});
@override
dynamic convert(BuildContext context, Map map, Map? methodMap,
{Domain? domain}) {
var name = map[tag];
switch (name) {
case 'FairFunction':
var appFunction =
convertAppFunction(context, map, methodMap, domain: domain);
if (appFunction != null) {
return appFunction;
}
var packagesFunction =
convertPackagesFunction(context, map, methodMap, domain: domain);
if (packagesFunction != null) {
return packagesFunction;
}
var flutterFunction =
convertFlutterFunction(context, map, methodMap, domain: domain);
if (flutterFunction != null) {
return flutterFunction;
}
var tag = FunctionDomain.getTag(map);
switch (tag) {
default:
}
break;
}
return super.convert(context, map, methodMap, domain: domain);
}
}
生成 dart:core
对应的 Sugar
在 build
方法体里面,对于一些基础类型的操作,是不识别的。比如, bool
取反。
!false
执行 bin/dart_core.dart
生成基础类型的语法糖。
现在你可以这样写:
SugarBool.invert(false)
对了,不要忘记把生成的 Sugar
文件,增加到 binding.dart
,然后再执行一次 app.dart
。
├─ bin
│ ├─ generated_module
│ │ ├─ binding.dart
│ │ ├─ app.dart
生成旧项目扩展对应的 Sugar
旧项目当中有一些同学喜欢用扩展方法,执行 bin/extension.dart
可以生成对应的语法糖。记得修改需要扫描扩展方法文件的路径。
生成 js
交互插件配置
前面我们写到要生成 js
中用于交互的插件 FairCommonPlugin
,需要手动修改 fair_common_plugin.dart
和 fair_basic_config.json
。
现在,你只用增加新的方法类,比如增加下面代码
import 'package:fair/fair.dart';
import 'package:flutter/foundation.dart';
mixin DebugPlugin implements FairCommonPluginMixin {
Future<dynamic> jsPrint(dynamic map) => request(
map,
(dynamic requestMap) async {
if (kDebugMode) {
debugPrint('来自js的参数:$requestMap');
}
return null;
},
);
}
然后执行 执行 bin/fair_common_plugin.dart
就会自动帮你修改 fair_common_plugin.dart
和 fair_basic_config.json
中的配置。
build.sh
执行项目下面的 build.sh
,会自动生成 Fair
产物,并且复制产物到 assets/fair
下面
new_page.sh
执行项目下面的 new_page.sh
,会自动生成页面跟产物的映射 asset_bunldes.dart
文件。
// 由 fair_gallery/bin/build.dart 生成
import 'package:fair_gallery/assets.dart';
final Map<String, String> _bundles = <String, String>{
'fair://FunctionDomainDemo':
Assets.assets_fair_lib_src_page_simple_function_domain_fair_bin,
'fair://PhotoGalleryItem':
Assets.assets_fair_lib_src_page_complex_photo_gallery_item_fair_bin,
'fair://PhotoGalleryPage':
Assets.assets_fair_lib_src_page_complex_photo_gallery_fair_bin,
'fair://PhotoGalleryPage1':
Assets.assets_fair_lib_src_page_complex_photo_gallery1_fair_json,
'fair://PhotoSwiper':
Assets.assets_fair_lib_src_page_complex_photo_swiper_fair_bin,
'fair://PluginDemo': Assets.assets_fair_lib_src_page_simple_plugin_fair_bin,
'fair://SugarDemo': Assets.assets_fair_lib_src_page_simple_sugar_fair_bin
};
extension FairBundleE on String {
String? get fairBundle => _bundles[this];
}
性能优化
流程判断语法
之前的语法糖中 Sugar.ifEqual
,Sugar.ifEqualBool
,Sugar.switchCase
的参数,是定义的时候就会去执行, 而不是根据判断条件去执行,这不仅仅是破坏了代码原有的意思,而且还引起性能问题。比如下面代码,不管到底是 true
还是 false
, trueValue
和 falseValue
都会执行,如果 trueValue
和 falseValue
中有跟 js
通信拿数据,这将是很昂贵的。
Sugar.ifEqualBool(
true,
trueValue: Container(
child: Text('成功'),
),
falseValue: Container(
child: Text('失败'),
)),
最新改动为:
Sugar.ifEqualBool(
true,
trueValue: () {
return Container(
child: Text('成功'),
);
}, falseValue: () {
return Container(
child: Text('失败'),
);
}),
当然 DynamicWidgetBuilder
当中也要做相应的修改。
下面举个🌰。
bool
的 &
和 |
判断,按照脚本执行生成的是这样的。
class SugarBool {
SugarBool._();
/// The logical conjunction ("and") of this and [other].
///
/// Returns `true` if both this and [other] are `true`, and `false` otherwise.
static bool and(bool input, bool other) => input & other;
/// The logical disjunction ("inclusive or") of this and [other].
///
/// Returns `true` if either this or [other] is `true`, and `false` otherwise.
static bool inclusiveOr(bool input, bool other) => input | other;
}
&
操作,我们知道,前面条件为false
后面是不需要做的,必然是false
。- 同理
|
操作,前面条件为true
后面也不需要做,必然为true
。
修改逻辑之后为。
class SugarBool {
SugarBool._();
/// The logical conjunction ("and") of this and [other].
///
/// Returns `true` if both this and [other] are `true`, and `false` otherwise.
static bool and(bool input, bool Function() other) => input & other();
/// The logical disjunction ("inclusive or") of this and [other].
///
/// Returns `true` if either this or [other] is `true`, and `false` otherwise.
static bool inclusiveOr(bool input, bool Function() other) => input | other();
}
对应 DynamicWidgetBuilder
中修改为;
class CustomDynamicWidgetBuilder extends DynamicWidgetBuilder {
CustomDynamicWidgetBuilder(
super.proxyMirror,
super.page,
super.bound, {
super.bundle,
});
@override
dynamic convert(BuildContext context, Map map, Map? methodMap,
{Domain? domain}) {
var name = map[tag];
switch (name) {
case 'SugarBool.and':
var p0 = pa0Value(pa0(map), methodMap, context, domain);
if (!p0) {
return false;
}
return pa0Value(
FunctionDomain.getBody(pa1(map)), methodMap, context, domain);
case 'SugarBool.inclusiveOr':
var p0 = pa0Value(pa0(map), methodMap, context, domain);
if (p0) {
return true;
}
return pa0Value(
FunctionDomain.getBody(pa1(map)), methodMap, context, domain);
default:
}
}
}
keframe
在例子 photo_gallery.dart 和 photo_gallery1.dart 中,我们分别使用了两种方法来实现列表。
-
利用
FairDelegate
绑定了列表的itemBuilder
,每个一个item
都是一个FairWidget
.
利用 FrameSeparateWidget
保存 item
的高度,防止向回滚动的时候列表抖动。
return FrameSeparateWidget(
index: index,
child: ExtendedFairWidget(
builder: (context) => PhotoGalleryItem(fairProps: fairProps),
name: Routes.fairPhotoGalleryItem.name,
fairProps: fairProps,
holder: (b) => SizedBox(height: height),
),
placeHolder: SizedBox(height: height),
);
Fair
已支持回调,把item
的代码写在一个文件里面。
SizeCacheWidget(
child: LoadingMoreList(
ListConfig(
itemBuilder:
(context, dynamic loadingMoreItem, loadingMoreIndex) {
return FrameSeparateWidget(
index: loadingMoreIndex,
placeHolder: Container(height: 200),
// 列表元素,实际上有 500 行
child: Container(),
);
},
),
),
),
理论上,第二种方式,更少的 FairWidget
,更少的 js
通信,更少的 json
序列化。应该更流畅的,但是实际上发现,更卡。同样 使用 FrameSeparateWidget
, 为啥第二种场景,会更卡呢?
实际上,问题出在 FrameSeparateWidget
的 child
属性。平时我们正常写代码的时候,创建一个 Widget
,可以说是非常廉价的。但是现在不同了,我们的 item
写在一个文件中,child
在 itembuilder
的时候就会全部创建,而这些组件里面都是跟 js
通信去获取值的操作,这个成本一下子就被无限放大了。
翻看 FrameSeparateWidget
代码,其实它也有占位,child
是在 scheduleTask
中才被挂靠到 Flutter
的树上面的。
void transformWidget() {
SchedulerBinding.instance.addPostFrameCallback((Duration t) {
FrameSeparateTaskQueue.instance!.scheduleTask(() {
if (mounted)
setState(() {
result = widget.child;
});
}, Priority.animation, () => !mounted, id: widget.index);
});
}
参考前面流程语法判断的改进,我们可以把这个 child
属性,改为一个 builder
回调,只有当真的要把内容挂靠到树上的时候才去创建。
create child only when it’s need (#21) · LianjiaTech/keframe@5ece6d5 (github.com) 这个 pr
已合并 ,但是还没有发布新的版本到 pub
。这里我把代码复制了出来,项目里面做了修改。
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:keframe/keframe.dart';
class ExtendedFrameSeparateWidget extends StatefulWidget {
const ExtendedFrameSeparateWidget({
Key? key,
this.index,
this.placeHolder,
required this.builder,
}) : super(key: key);
final WidgetBuilder builder;
/// The placeholder widget sets components that are as close to the actual widget as possible
final Widget? placeHolder;
/// Identifies its own ID, used in a scenario where size information is stored
final int? index;
@override
State<ExtendedFrameSeparateWidget> createState() =>
_FrameSeparateWidgetState();
}
class _FrameSeparateWidgetState extends State<ExtendedFrameSeparateWidget> {
Widget? result;
@override
void initState() {
super.initState();
result = widget.placeHolder ??
Container(
height: 20,
);
final Map<int?, Size>? size = SizeCacheWidget.of(context)?.itemsSizeCache;
Size? itemSize;
if (size != null && size.containsKey(widget.index)) {
itemSize = size[widget.index];
logcat('cache hit:${widget.index} ${itemSize.toString()}');
}
if (itemSize != null) {
result = SizedBox(
width: itemSize.width,
height: itemSize.height,
child: result,
);
}
transformWidget();
}
@override
void didUpdateWidget(ExtendedFrameSeparateWidget oldWidget) {
super.didUpdateWidget(oldWidget);
transformWidget();
}
@override
Widget build(BuildContext context) {
return ItemSizeInfoNotifier(index: widget.index, child: result);
}
void transformWidget() {
SchedulerBinding.instance.addPostFrameCallback((Duration t) {
FrameSeparateTaskQueue.instance!.scheduleTask(() {
if (mounted) {
setState(() {
result = widget.builder(context);
});
}
}, Priority.animation, () => !mounted, id: widget.index);
});
}
}
利用 Flutter
树 从内存里面获取值
在例子 photo_gallery.dart 中每个 item
的数据是通过 FairWidget
传递过去的。我们可以将数据放到ShareDataWidget
中。
ShareDataWidget(
child: FairWidget(),
data: fairProps,
index: widget.index,
)
ShareDataWidget
的实现如下:
import 'package:flutter/material.dart';
class ShareDataWidget extends StatelessWidget {
const ShareDataWidget({
super.key,
required this.child,
required this.data,
this.index,
});
final dynamic data;
final int? index;
final Widget child;
/// get data
static dynamic of(BuildContext context) {
return context.findAncestorWidgetOfExactType<ShareDataWidget>()?.data;
}
static int? getIndex(BuildContext context) {
return context.findAncestorWidgetOfExactType<ShareDataWidget>()?.index;
}
static dynamic getValue(BuildContext context, List<dynamic> keys,
{dynamic defaultValue}) {
var data = context.findAncestorWidgetOfExactType<ShareDataWidget>()?.data;
if (data != null) {
for (var key in keys) {
if (data != null && data[key] != null) {
data = data[key];
}
}
}
return data ?? defaultValue;
}
@override
Widget build(BuildContext context) {
return child;
}
}
这里的话,就可以直接通过这种方法拿到对应的数据。
ShareDataWidget.getValue(
context,
['item', 'tags'],
defaultValue: [],
)
帧率对比
通过减少不必要的跟 js
的通信,一个逻辑比较复杂的列表,在真机上面的帧率,aot
和 fair
不分上下。
AOT:
Fair:
大家可以下载 apk
,自行体验。通过点击右上角,切换 Fair 模式
和 Aot 模式
。
更多细节
flatbuffers
65 层限制
flatc
支持 json
嵌套有默认上限 65
.
maximum parsing depth 65 reached · Issue #7895 · google/flatbuffers (github.com)
根据官方提示,我在 flatbuffers/CMakeLists.txt at master · google/flatbuffers (github.com) 中找到了参数 FLATBUFFERS_MAX_PARSING_DEPTH
。
就是说我们不能直接用官方的打出来的产物了,需要自己生成 flatc
。
根据 FlatBuffers 环境安装 | FAIR (58.com) 操作步骤,在点 Generate
之前,点击 Add Entry
增加 FLATBUFFERS_MAX_PARSING_DEPTH
参数, 再点一下 Confiure
, 然后点击 Generate
, 后面的步骤一致。
生成的 flatc
就能转成更大的 json
产物了。
安卓
安卓是利用 eclipsesource/J2V8: Java Bindings for V8 (github.com) 库加载 js
的,打包的时候记得增加 proguard-rules.pro
, 防止被混淆。
-keep class com.eclipsesource.v8.** { *; }
全量映射
zmtzawqlp/fair_gallery: fair 🌰 (github.com) 中使用的是全量的映射。并且当前 Fair
用于查找映射的 map
改成了 SplayTreeMap
(自平衡二叉搜索树),它可以快速访问最近被访问的元素。理论上对于匹配性能影响不大,但是我觉得还是应该针对个人的情况,对一些不会使用的映射进行排除。
- 移除映射中的
Icon
。 比如项目中就不会使用官方的Icon
,自己公司的UI
都会提供对应的图片。移除它们,能减少包体积.(Flutter
打包会做tree-shake-icons
)。
├─ bin
│ ├─ generated_module
│ │ ├─ flutter.dart
打开 bin/generated_module/flutter.dart
, 在 createBindings
方法的 componentSkips
中增加 ^Icons.*
,该参数支持正则表达式。
await createBindings(
componentSkips: {
// dart:ui 里面的
'Gradient.linear',
'Gradient.radial',
'Gradient.sweep',
// 支持正则
'^Icons.*',
},
)
做了这一步操作之后,再次执行脚本,apiCount
数量从 12681
降低到了 3985
。
^Diagnostic*
诊断相关的。^Render*
Render
系列的东西,几乎不可能在build
方法体里面出现。^CupertinoIcons.*
,同Icons
。- 再比如,你的项目只支持
android
,ios
,那么windows
,mac
,web
,fuchsia
的api
就用不上了。你可以排除它们。
-
^RawKeyEventDataMacOs.*
-
^RawKeyEventDataWeb.*
-
^RawKeyEventDataWindows.*
-
^RawKeyEventDataFuchsia.*
最后,建议先一个一个执行,看看区别,如果 widgetCount
发生改变,那么要确认这个 widget
是不是真的不用了。整个操作下来,减少了 1万
个映射。
当然,每个项目的需求不一样,大家可以按照自己的需求,生成自己想要的映射。
泛型
dart
是强类型语言, 映射中的泛型,我们只能改为 dynamic
或者 Object
。
栗子1
// typedef LoadingMoreItemBuilder<in T> = Widget Function(BuildContext context, T item, int index)
// package:loading_more_list/src/list_config/loading_more_list_config.dart
case 'Widget Function(BuildContext, dynamic, int)':
使用的时候:
itemBuilder:
(context, dynamic loadingMoreItem, loadingMoreIndex) {
}
栗子2
SugarList.generate(10, ((p0) => Text('$p0')))
这个回调是泛型,T Function(int)
, 我们生成的映射是 dynamic Function(int)
。
因为 analyzer
会去推导类型, 生成的 json
是 Widget Function(int)
,这样子就没法匹配上 dynamic Function(int)
。
我们需要使用 Sugar.asT
, 让 analyzer
推导出来 dynamic
的结果。
SugarList.generate(10, ((p0) => Sugar.asT<dynamic>(Text('$p0'))));
异步
对于异步回调,Fair
提供了 CompleterPlugin
和对应的语法糖 Sugar.createFuture
来实现。
栗子1
RefreshIndicator(
onRefresh: () => Sugar.createFuture<void>(
function: _onRefresh,
argument: '可以传一些参数过去,多个参数用数组',
),
RefreshIndicator
的 onRefresh
是一个 Future<void> Function()
。
Sugar.createFuture
的参数如下:
static Future<T> createFuture<T>({
String? futureId,
// 一个入参,结构如下
// {
// 'futureId': futureId,
// 'argument': argument,
// }
Function? function,
// 多个用数组,别用 Map,会跟解析冲突
dynamic argument,
// 执行完之后的回调
Function? callback,
})
js
部分
void _onRefresh(Map input) {
String futureId = input['futureId'];
// 可以传一些参数过去,多个参数用数组
String argument = input['argument'];
// 模拟一个耗时的操作,等操作完毕之后,再去完成 Future
FairCommonPlugin().futureDelayed({
// required
'pageName': _pageName,
'seconds': 2,
'callback': (dynamic result) {
FairCommonPlugin().futureComplete({
// required
'pageName': _pageName,
'futureId': futureId,
'futureValue': null,
});
}
});
}
Sugar.createFuture
会生成一个唯一的 id
,和方法入参(如果有) 一起传递到 js
里面,等 js
当中执行完毕耗时任务,再调用 FairCommonPlugin().futureComplete
完成 future
。
由于 future
的值是泛型,目前 Fair
内置支持的泛型包括,int
, double
, bool
, String
以及它们的空类型和 List
类型。 既 Future<int>
, Futrue<int?>
, Future<List<int>>
,Futrue<List<int>?>
。
栗子2
Fair
还提供了 Sugar.futureValue
,Sugar.futureVoid
,提供同步直接返回数据。
LikeButton(
onTap: (isLiked) => Sugar.futureValue(SugarBool.invert(isLiked)),
)
栗子3
如果 future
结束之后。你还想做什么操作,那怎么做呢?
Sugar.createFuture
提供了 callback
回调,将 future
执行完毕之后的结果,再次回调回来。
下面的例子,演示了如何将参数传入 js
,再经过参数再回传回来,将内存里面的数据修改掉。
LikeButton(
onTap: (isLiked) => Sugar.createFuture<bool?>(
function: _likeButtonTap,
argument: isLiked,
callback: (result) => SugarMap.set(
loadingMoreItem,
'is_favorite',
result,
),
),
)
void _likeButtonTap(Map input) {
String futureId = input['futureId'];
// isLiked
bool isLiked = input['argument'];
FairCommonPlugin().futureComplete({
// required
'pageName': _pageName,
'futureId': futureId,
'futureValue': !isLiked,
});
}
栗子4
一些三方库,有自己的一套机制,很难跟 js
进行互动,比如 loading_more_list | Flutter Package (flutter-io.cn) 的数据源获取是单独封装在 LoadingMoreBase
当中的,如果我们只能写死这个类,那么我们就失去了动态化的能力。
为此做了以下封装,LoadingMoreRepository
是一个通用的加载类,把数据加载通过 _onLoadData
暴露出去,并且绑定上 js
中的方法。
import 'dart:async';
import 'package:fair/fair.dart';
import 'package:loading_more_list/loading_more_list.dart';
class LoadingMoreRepository extends LoadingMoreBase<dynamic> {
LoadingMoreRepository();
bool _hasMore = true;
@override
bool get hasMore =>
_hasMore && (_maxLength != null ? length < _maxLength! : true);
DateTime get dateTimeNow => _dateTimeNow;
DateTime _dateTimeNow = DateTime.now();
void Function(Map input)? _onLoadData;
int? _maxLength;
bool? _notifyStateChanged;
static LoadingMoreRepository onLoadData(
LoadingMoreRepository repository,
void Function(Map input) onLoadData, {
bool? notifyStateChanged,
int? maxLength,
}) {
repository._onLoadData = onLoadData;
repository._maxLength = maxLength;
repository._notifyStateChanged = notifyStateChanged;
return repository;
}
@override
Future<bool> refresh([bool notifyStateChanged = false]) async {
_hasMore = true;
final bool result =
await super.refresh(_notifyStateChanged ?? notifyStateChanged);
_dateTimeNow = DateTime.now();
return result;
}
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
try {
String futureId =
'LoadingMoreRepository${DateTime.now().microsecondsSinceEpoch}';
Completer completer = CompleterPlugin.createCompleter(futureId);
_onLoadData?.call({
'futureId': futureId,
'isloadMoreAction': isloadMoreAction,
});
var response = await completer.future;
if (response == null) {
return false;
}
var statusCode = response['statusCode'];
var pageIndex = response['pageIndex'];
if (pageIndex == 1) {
clear();
}
if (statusCode != 200) {
return false;
}
var list = response['list'] ?? [];
addAll(list);
_hasMore = list.isNotEmpty;
return true;
} catch (exception) {
return false;
}
}
}
使用的时候将 js
中的 _onLoadData
方法绑定到 _repository
上面。
LoadingMoreRepository.onLoadData(
_repository,
_onLoadData,
maxLength: 300,
)
js
方法里面获取到了数据再通过 FairCommonPlugin().futureComplete
把结果返回。
int pageIndex = 0;
final List<int> _itemIds = <int>[];
void _onLoadData(Map input) {
String futureId = input['futureId'];
bool isloadMoreAction = input['isloadMoreAction'];
var url = '';
if (!isloadMoreAction) {
_itemIds.clear();
pageIndex = 0;
}
if (_itemIds.isEmpty) {
url = 'https://api.tuchong.com/feed-app';
} else {
var lastId = _itemIds[_itemIds.length - 1];
// ignore: prefer_interpolation_to_compose_strings
url = 'https://api.tuchong.com/feed-app?post_id=' +
lastId.toString() +
'&page=' +
pageIndex.toString() +
'&type=loadmore';
}
FairCommonPlugin().http({
'method': 'GET',
'url': url,
// required
'pageName': _pageName,
// if need, add a callback
'callback': (dynamic result) {
if (result != null) {
var statusCode = result['statusCode'];
var list = [];
if (statusCode == 200) {
var map = result['json'];
if (map != null) {
var feedList = map['feedList'];
for (var i = 0; i < feedList.length; i++) {
var item = feedList[i];
var postId = item['post_id'];
if (!_itemIds.contains(item[postId])) {
list.add(item);
_itemIds.add(postId);
}
}
}
pageIndex = pageIndex + 1;
}
if (!isloadMoreAction) {
lastRefreshTime = DateTime.now();
}
FairCommonPlugin().futureComplete({
// required
'pageName': _pageName,
'futureId': futureId,
'futureValue': {
'pageIndex': pageIndex,
'statusCode': statusCode,
'list': list,
},
});
}
},
});
}
这里就完成了列表数据请求完全动态化的需求。
Flutter
对象在 Fair
中使用
Flutter
对象是没法在 Fair
中识别的。那我们遇到这种情况怎么处理 ?
栗子1
return LayoutBuilder(
builder: (context, boxConstraints) {
return Center(
child: Text(''),
);
},
);
这种场景比较常见吧?根据 boxConstraints
的最大宽度,最大高度,做一些布局。
这种情况只能做预埋了。比如 Fair
提供了 Sugar.boxConstraintsToMap
方法将 BoxConstraints
转换成 map
值。
return LayoutBuilder(
builder: (context, boxConstraints) {
return Center(
child: Text(
SugarDouble.doubleToString(
SugarMap.get(
Sugar.boxConstraintsToMap(boxConstraints), 'maxWidth'),
),
),
);
},
);
栗子2
对于其他 Fair
不支持的,你可以添加自己的语法糖。
static void Function(TapDownDetails details) onTapDown(Function function) {
return (TapDownDetails details) {
function.call([
details.globalPosition.dx,
details.globalPosition.dy,
details.kind?.name,
]);
};
}
var _details;
void _onTapDown(dynamic details) {
_details = details;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: GestureDetector(
onTapDown: SugarCommon.onTapDown(_onTapDown),
child: Container(
width: 50,
height: 50,
color: Colors.red,
child: Text('$_details'),
),
),
),
);
}
js 能调试吗?
答案是没法调试 js
,你根本不知道自己错在哪里。fair_gallery
中提供了 jsPrint
,可以通过打印来调试。
FairCommonPlugin().jsPrint({
'pageName': _pageName,
'info': 'xxx',
});
当然你也可以在生成的 js
文件里面直接用 js
语法进行打印。
console.log('结果');
面向打印编程!
对于不熟悉 js
的盆友来说,你并不知道你自己写的 dart
,是否可能正确的转换成 js
。为此,Fair
增加了大量的适配,建议写代码之前看看下面这些 js
,心中有数才不慌。
在 Fair
中操作 dart
对象
由于 Fair
是将 ui
转换成 json
,将逻辑转换成了 js
。这就意味着我们没法直接操作 dart
对象。当我们想要创建和操作 TabController
, AnimationController
, ScrollController
,ValueNotifier
等对象的时候,通过 FairDelegate
去绑定是很麻烦的,也没办法动态化,更不好跟 js
交互。那么有没有通用简单的方法来做呢?
利用 BuildContext
我们创建对象,并且要能获取它,这不就是 数据共享
,是不是听起来有点像现在的一些框架了?我们可以通过 BuildContext
做一些骚操作。
在 fair_gallery/listenable_scope.dart at main · zmtzawqlp/fair_gallery (github.com) 中, 实现了对常见的 Listenable
的创建,存储,获取。
ListenableScope(
addListener: _addListener,
onCreateKey: _onCreateKey,
uniqueKey: 'ListenableScopeDemo',
configs: [
ListenableScopeConfig(type: 'ScrollController'),
ListenableScopeConfig(
type: 'AnimationController',
addListener: true,
),
// 有重复的类型,请用 tag 区分
ListenableScopeConfig(
type: 'TabController',
addListener: true,
tag: '0',
),
ListenableScopeConfig(
type: 'ValueNotifier',
addListener: true,
),
// 有重复的类型,请用 tag 区分
ListenableScopeConfig(
type: 'TabController',
tag: '1',
),
],
onCreate: (String key, TickerProvider vsync) {
return Sugar.switchCase(
key,
[
SugarSwitchCaseObj(
sugarCase: () => 'ScrollController',
reValue: () => ScrollController(),
),
SugarSwitchCaseObj(
sugarCase: () => 'AnimationController',
reValue: () => AnimationController(
vsync: vsync,
value: 50.0,
lowerBound: 50.0,
upperBound: 100.0,
duration: const Duration(seconds: 2),
reverseDuration: const Duration(seconds: 2),
),
),
SugarSwitchCaseObj(
sugarCase: () => 'TabController0',
reValue: () => TabController(vsync: vsync, length: 2),
),
SugarSwitchCaseObj(
sugarCase: () => 'ValueNotifier',
reValue: () => ValueNotifier(1.0),
),
SugarSwitchCaseObj(
sugarCase: () => 'TabController1',
reValue: () => TabController(vsync: vsync, length: 3),
),
],
() => Sugar.nullValue(),
);
},
builder: (BuildContext context) {
return Container();
},
);
- 通过
configs
去配置需要创建的类型。 - 通过
onCreate
回调,根据自己的情况创建对应的对象 - 通过
_addListener
增加对这些Listenable
的监听。
void _addListener(dynamic value) {
var type = value[0];
var values = value[1];
FairCommonPlugin().jsPrint(
{
'pageName': _pageName,
'type': type,
'values': values,
},
);
}
Listenable
对象的值,都转成了 Map
, 以便 js
中获取。比如:
if (dartObject is TabController) {
return {
'index': dartObject.index,
'indexIsChanging': dartObject.indexIsChanging,
'offset': dartObject.offset,
'previousIndex': dartObject.previousIndex,
'length': dartObject.length,
};
}
- 通过
_onCreateKey
来保存唯一标识,以便js
中交互。
// 用于去内存里面获取 ListenableScope
String _uniqueKey = '';
void _onCreateKey(String key) {
_uniqueKey = key;
}
builder
方法体里面获取创建的对象
因为在 fair_gallery/listenable_scope.dart at main · zmtzawqlp/fair_gallery (github.com) 的 builder
回调,我们很容易通过 context
找到 _ListenableScopeState
。
@override
Widget build(BuildContext context) {
return Builder(builder: (context) {
return widget.builder(context);
});
}
比如我们可以在 builder
中找到刚才定义的 ValueNotifier
。
builder: (BuildContext context) {
return ValueListenableBuilder(
valueListenable: ListenableScope.of<ValueNotifier>(
context,
'ValueNotifier',
),
builder: (context, dynamic value, child) {
return Text(
SugarString.concatenates(
'ValueNotifier 的值为:',
'$value',
),
);
},
);
}
也可以通过 Sugar.mapGet
+ Sugar.dartObjectToMap
的组合获取到对象的某个值。
return Container(
padding: const EdgeInsets.all(8),
alignment: Alignment.center,
height: 50,
width: Sugar.mapGet(
Sugar.dartObjectToMap(
ListenableScope.of<AnimationController>(
context,
'AnimationController',
),
),
'value',
),
color: Colors.blue,
child: const Text(
'点我开始动画!',
style: TextStyle(
color: Colors.white,
),
),
);
通过 在 js
中交互
通过 fair_gallery/listenable_scope.dart at main · zmtzawqlp/fair_gallery (github.com) ,我们可以实现在 js
中跟内存里的 dart
对象进行交互。
ListenableScopePlugin
中部分代码:
mixin ListenableScopePlugin implements FairCommonPluginMixin {
Future<dynamic> scrollController(dynamic map) => request(
map,
(dynamic requestMap) async {
var uniqueKey = requestMap['uniqueKey'];
var type = requestMap['type'];
var method = requestMap['method'];
var parameter = requestMap['parameter'];
var scrollController =
ListenableScope.get(uniqueKey, type) as ScrollController?;
if (scrollController != null) {
switch (method) {
case 'animateTo':
var offset = parameter['offset'].toDouble();
var duration = parameter['duration'];
// 从 js 过来
if (duration is! Duration) {
duration = Sugar.durationFromJs(duration);
}
var curve = parameter['curve'];
// Curves.bounceIn
if (curve is! Curve) {
curve = flutterComponents[curve.toString()];
}
scrollController.animateTo(
offset,
duration: duration,
curve: curve,
);
break;
case 'jumpTo':
scrollController.jumpTo(parameter.toDouble());
break;
case 'get':
return Sugar.dartObjectToMap(scrollController);
default:
}
}
return null;
},
);
}
在 js
中调用示例:
将之前保存的 _uniqueKey
传入,用于在内存中寻找 _ListenableScopeState
。传入在 ListenableScopePlugin
中约定好的参数。
void _onScrollControllerAnimateTo(double offset) {
FairCommonPlugin().scrollController({
'pageName': _pageName,
'uniqueKey': _uniqueKey,
'type': 'ScrollController',
'method': 'get',
'callback': (values) {
if (values['hasClients'] == true) {
FairCommonPlugin().scrollController({
'pageName': _pageName,
'uniqueKey': _uniqueKey,
'type': 'ScrollController',
'method': 'animateTo',
'parameter': {
'offset': offset,
'duration': const Duration(seconds: 1),
'curve': 'Curves.bounceIn',
},
});
}
}
});
}
状态管理框架
按照前面的步骤,理论上面,我们可以去包装任意的状态管理框架,但是要注意,这些框架有依赖于泛型,在实际操作中,避免直接使用泛型的 runtimeType
的字符串跟其他字符串比较,因为有些项目是做了混淆的。状态管理框架太多了,这里不做细说。欢迎 pr
各状态框架的例子。
ios-framework 混合开发打包
由于 flutter build ios-framework ignore plugin vendored_frameworks · Issue #125530 · flutter/flutter (github.com)的问题,产物中不会包含 FairDynamicFlutter.framework
,解决方法就是在产物生成之后,自己把 FairDynamicFlutter.framework
复制到产物中,具体可以参考 混合开发模式下,提示找不到FairDynamicFlutter · Issue #143 · wuba/Fair (github.com)
DateTime 和 Duration
Fair
支持在 js
中写 DateTime
和 Duration
,不过在从 js
中获取到的是 Map
,在 build
方法体里面使用的时候需要进行一下转换。你可以使用 Sugar.durationFromJs
和 Sugar.dateTimeFromJs
。
结语
在做好提前预埋映射的前提下,Fair
可以实现大部分操作。虽不完美,但也是值得入手的热更新框架。希望有更多的好的想法能汇集在一起,让 Flutter
的热更新不再被诟病。
最近 ChatGPT
很火很强, 也许 zmtzawqlp/fair_gallery: fair 🌰 (github.com) 脚本都能用它来实现,也许以后很多的工作也会被它代替。但是,想象力和创造力是人类特有的能力,这是目前 AI
无法替代的。在这样一个 AI
时代,善于提问,不要只是拘泥于自己现在掌握的技术,保持对新技术的敏感力,努力做一个能掌控 AI
工具的人。无论是 ChatGPT
,还是未来的 AI
,它们其实都是人类的投影。它们多厉害,取决于你自己有多厉害。
半年没写东西了,有点偏题了,写完已经是 5月4号 青年节
。也许,生活会磨平了一切的棱角,改变我们的模样。但是哪怕只是一个黑八,你也还是会觉得炒鸡叼;灌篮高手,迟到的结局,才感觉青春本来就是不完美;Dota4 更新,Dead Game 原来还是泰裤辣;夜曲一响,依然会悄悄哼唱;糖果大宝一开腔,群里消息+99。我们都在变老,但是一些东西却不会变!
愿你出走半生,归来仍是少年 !
原文链接:https://juejin.cn/post/7228967938473394213 作者:法的空间