Flutter 热更新 Fair 真.体验

22,218 阅读25分钟

“我正在参加「掘金·启航计划」”

前言

zmtzawqlp/fair_gallery at v3.13.4 (github.com) 适配 Flutter 3.13.4 zmtzawqlp/fair_gallery: fair 🌰 (github.com) 适配 Flutter 3.3.9

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 下发热更新的需求。
  • 有一套开源的热更新服务平台。

最新更新

文章后续内容都是基于 zmtzawqlp/fair (github.com)

  • main 分支基于 3.13 以下
  • fixed 分支基于 3.13 以及以上

准备环境

Flatbuffers

Fair 的产物中组件映射是一个 json,但是我们可以选择生成 .bin,它是由 google/flatbuffers: FlatBuffers: Memory Efficient Serialization Library (github.com)(解码速度极快,内存使用效率高) 生成。flatbuffers/dart 来提供解析。

我们将 Flatbuffers 官方打包好的产物下载下来。

并且在环境变量中进行配置。

  • windows image.png

  • mac 截屏2023-04-03 19.48.05.png

配置好之后,使用 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 文件夹里面就会生成对应的产物。

截屏2023-04-17 16.16.49.png

  • 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#")));
  • #FairKey# 这个实际上是唯一的页面名字,会在这里被替换。

  • #FairProps# 这个实际上是这个页面的入参,会在这里被替换。

截屏2023-04-17 15.09.32.png

实际上一个完整生命周期的页面如下:

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');
        }                        
    }
}

截屏2023-04-17 16.26.29.png

使用

我们增加在 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) 展现更多使用过程中场景。

1683132023922.gif

配置

使用的 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 的全量映射。

截屏2023-04-18 11.01.04.png

  • flutter.function.dart 为 flutter.bindings.dart 中涉及到回调方法的全量映射。

截屏2023-04-18 11.03.17.png

生成 三方库 映射
├─ bin
│  ├─ generated_module
│  │  ├─ binding.dart 
│  │  ├─ packages.dart 

执行该路径下的 packages.dart ,会读取 binding.dart 里面的三方引用。binding.dart 类似 FairBinding,我没有用注解,是因为这样的话,有引用提示。

截屏2023-04-18 11.08.09.png

生成 packages.bindings.dart 和 packages.function.dart 文件。

├─ lib
│  └─ src
│     ├─ generated_module
│     │  ├─ packages.bindings.dart
│     │  ├─ packages.function.dart
  • packages.bindings.dart 为你当前使用三方库的全量映射。

截屏2023-04-18 11.09.33.png

  • packages.function.dart 为 packages.bindings.dart 中涉及到回调方法的全量映射。

截屏2023-04-18 11.10.50.png

生成 本项目 映射
├─ bin
│  ├─ generated_module
│  │  ├─ binding.dart 
│  │  ├─ app.dart 

执行该路径下的 app.dart ,会读取 binding.dart 里面的本项目中的引用。binding.dart 类似 FairBinding,我没有用注解,是因为这样的话,有引用提示。

截屏2023-04-18 11.08.09.png

生成 app.bindings.dart 和 app.function.dart 文件。

├─ lib
│  └─ src
│     ├─ generated_module
│     │  ├─ app.bindings.dart
│     │  ├─ app.function.dart
  • app.bindings.dart 为你当前使用项目中需要生成映射的文件的全量映射。

截屏2023-04-18 11.15.00.png

截屏2023-04-18 11.16.02.png

  • 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 生成基础类型的语法糖。

截屏2023-04-12 22.06.31.png

现在你可以这样写:

SugarBool.invert(false)  

对了,不要忘记把生成的 Sugar 文件,增加到 binding.dart,然后再执行一次 app.dart。

├─ bin
│  ├─ generated_module
│  │  ├─ binding.dart 
│  │  ├─ app.dart 

生成旧项目扩展对应的 Sugar

旧项目当中有一些同学喜欢用扩展方法,执行 bin/extension.dart 可以生成对应的语法糖。记得修改需要扫描扩展方法文件的路径。

截屏2023-04-18 15.09.33.png

生成 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 .

截屏2023-04-18 22.09.04.png

利用 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(),
            );
          },
        ),
      ),
    ),    

截屏2023-04-18 22.37.47.png

理论上,第二种方式,更少的 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:

截屏2023-03-25 23.48.59.png

Fair:

截屏2023-03-25 23.42.55.png

大家可以下载 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。

截屏2023-04-26 15.54.42.png

就是说我们不能直接用官方的打出来的产物了,需要自己生成 flatc 。

根据 FlatBuffers 环境安装 | FAIR (58.com) 操作步骤,在点 Generate 之前,点击 Add Entry 增加 FLATBUFFERS_MAX_PARSING_DEPTH 参数, 再点一下 Confiure , 然后点击 Generate, 后面的步骤一致。

截屏2023-04-26 15.36.35.png

生成的 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。

截屏2023-04-18 19.59.31.png

  • ^Diagnostic* 诊断相关的。
  • ^Render* Render 系列的东西,几乎不可能在 build 方法体里面出现。
  • ^CupertinoIcons.*,同 Icons。
  • 再比如,你的项目只支持 android,ios,那么 windows, mac, web, fuchsia 的 api 就用不上了。你可以排除它们。
  1. ^RawKeyEventDataMacOs.*

  2. ^RawKeyEventDataWeb.*

  3. ^RawKeyEventDataWindows.*

  4. ^RawKeyEventDataFuchsia.*

最后,建议先一个一个执行,看看区别,如果 widgetCount 发生改变,那么要确认这个 widget 是不是真的不用了。整个操作下来,减少了 1万 个映射。

截屏2023-04-19 11.17.49.png

截屏2023-04-19 11.18.14.png

当然,每个项目的需求不一样,大家可以按照自己的需求,生成自己想要的映射。

泛型

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,心中有数才不慌。

截屏2023-04-19 15.14.51.png

在 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。我们都在变老,但是一些东西却不会变!

愿你出走半生,归来仍是少年 !