Flutter 热更新 Fair 真.体验

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

前言

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 Flutter 热更新 Fair 真.体验

  • mac Flutter 热更新 Fair 真.体验

配置好之后,使用 Fair 就会生成对应的 bin 文件。

版本

fair_version 主要是对 flutter sdk 中的组件做了映射,最新版本支持 flutterstable 分支 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 文件夹里面就会生成对应的产物。

Flutter 热更新 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();
      },
    );

原理

简单的来说,Fairbuild 方法体里面的组件映射成了 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": {}
}

这个 _controllerFair 会去哪里找呢?

自定义 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# 这个实际上是这个页面的入参,会在这里被替换。

Flutter 热更新 Fair 真.体验

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

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 会在 FairWidgetdidChangeDependencies 方法执行的时候通过 BasicMessageChannel 去通知到 js 当中的 onLoad 方法。

  • onUnload 会在 FairWidgetdispose 方法执行的时候通过 BasicMessageChannel 去通知到 js 当中的 onUnload 方法。

通过上面的代码,我们同一个页面可以同时支持 Flutter aotjs 的逻辑。

IFairPlugin

通过上面所知,build 方法体外面的代码是需要能被转换成 js 的才行。这明显不能满足我们需要跟 Flutter api 交互的需求。

为此,Fair 提供了 jsFlutter 进行交互的方法。

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

Flutter 热更新 Fair 真.体验

使用

我们增加在 onLoad 方法中去请求接口。注意,当中的 pageNamecallback 是固定的名称,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 方法中, ExtendedImagetrue ,表明了它是一个组件(Widget)。CropAspectRatios.xxxx 则不是。

自定义自己的 AppGeneratedModule

有些时候,Fair 生成的映射会有缺失或者错误,定义一个自己的 AppGeneratedModule 并且继承 FairAppGeneratedModule,来满足我们自定的需求,并且不影响 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 热更新 Fair 真.体验

配置

使用的 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 文件的来源,以及解析,决定是否要缓存解析结果。其中的 decodeisFlexBuffer 代表是 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 ,你就知道这个组件是什么东西。methodMapjson 中生成的针对 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.dartflutter.function.dart 文件。

├─ lib
│  └─ src
│     ├─ generated_module
│     │  ├─ flutter.bindings.dart
│     │  ├─ flutter.function.dart
  • flutter.bindings.dart 为你当前使用 Flutter SDK 以及 dart:ui 的全量映射。

Flutter 热更新 Fair 真.体验

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

Flutter 热更新 Fair 真.体验

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

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

Flutter 热更新 Fair 真.体验

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

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

Flutter 热更新 Fair 真.体验

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

Flutter 热更新 Fair 真.体验

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

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

Flutter 热更新 Fair 真.体验

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

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

Flutter 热更新 Fair 真.体验

Flutter 热更新 Fair 真.体验

  • app.function.dartapp.bindings.dart 中涉及到回调方法的全量映射。
使用

生成好这些映射之后,我们就要去使用它们了。

移除对 fair_versionFlutter SDK 的依赖

我们将不再依赖 fair_version 的对应的 Flutter SDK 版本,我们将引用 fair_versionflutter_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 生成基础类型的语法糖。

Flutter 热更新 Fair 真.体验

现在你可以这样写:

SugarBool.invert(false)  

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

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

生成旧项目扩展对应的 Sugar

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

Flutter 热更新 Fair 真.体验

生成 js 交互插件配置

前面我们写到要生成 js 中用于交互的插件 FairCommonPlugin,需要手动修改 fair_common_plugin.dartfair_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.dartfair_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.ifEqualSugar.ifEqualBoolSugar.switchCase 的参数,是定义的时候就会去执行, 而不是根据判断条件去执行,这不仅仅是破坏了代码原有的意思,而且还引起性能问题。比如下面代码,不管到底是 true 还是 falsetrueValuefalseValue 都会执行,如果 trueValuefalseValue 中有跟 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.dartphoto_gallery1.dart 中,我们分别使用了两种方法来实现列表。

  • 利用 FairDelegate 绑定了列表的 itemBuilder ,每个一个 item 都是一个 FairWidget .

Flutter 热更新 Fair 真.体验

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

Flutter 热更新 Fair 真.体验

理论上,第二种方式,更少的 FairWidget,更少的 js 通信,更少的 json 序列化。应该更流畅的,但是实际上发现,更卡。同样 使用 FrameSeparateWidget, 为啥第二种场景,会更卡呢?

实际上,问题出在 FrameSeparateWidgetchild 属性。平时我们正常写代码的时候,创建一个 Widget ,可以说是非常廉价的。但是现在不同了,我们的 item 写在一个文件中,childitembuilder 的时候就会全部创建,而这些组件里面都是跟 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 的通信,一个逻辑比较复杂的列表,在真机上面的帧率,aotfair 不分上下。

AOT:

Flutter 热更新 Fair 真.体验

Fair:

Flutter 热更新 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

Flutter 热更新 Fair 真.体验

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

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

Flutter 热更新 Fair 真.体验

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

Flutter 热更新 Fair 真.体验

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

  2. ^RawKeyEventDataWeb.*

  3. ^RawKeyEventDataWindows.*

  4. ^RawKeyEventDataFuchsia.*

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

Flutter 热更新 Fair 真.体验

Flutter 热更新 Fair 真.体验

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

泛型

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 会去推导类型, 生成的 jsonWidget 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: '可以传一些参数过去,多个参数用数组',
    ),    

RefreshIndicatoronRefresh 是一个 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.futureValueSugar.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,心中有数才不慌。

Flutter 热更新 Fair 真.体验

Fair 中操作 dart 对象

由于 Fair 是将 ui 转换成 json ,将逻辑转换成了 js。这就意味着我们没法直接操作 dart 对象。当我们想要创建和操作 TabController, AnimationController, ScrollControllerValueNotifier 等对象的时候,通过 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 中写 DateTimeDuration,不过在从 js 中获取到的是 Map,在 build 方法体里面使用的时候需要进行一下转换。你可以使用 Sugar.durationFromJsSugar.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 作者:法的空间

(1)
上一篇 2023年5月4日 上午10:36
下一篇 2023年5月4日 上午10:47

相关推荐

发表回复

登录后才能评论