从零开始写一个demo

Wuba2022年11月19日大约 8 分钟

本小节的内容将带领你从零开始完成一个 Fair 的 demo,请系好安全带!

学习很多语言的时候都有 hello world,在 Flutter 里面他的 Hello World 是个 counting 计数器。下面将演示如何把这个计数器,改造为一个 Fair 的动态页面。

main.dart 里面默认的 counting 代码(代码里面的默认注释被去掉了):

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  
  final String title;
  
  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

改造分为 7 步。

step1.将 FairApp 添加为需要动态化部分的顶级节点

常见做法是作为 App 的根节点,如果不是全局采用也可以作为子页面的根节点。

void main() {
  // runApp(MyApp());

  WidgetsFlutterBinding.ensureInitialized();

  FairApp.runApplication(
    _getApp(),
    plugins: {},
  );
}

dynamic _getApp() => FairApp(
  modules: {},
  delegate: {},
  child: MyApp(),
);

step2.使用 @FairPatch() 注解标记需要动态化的 Widget

对于需要进行动态化改造的 Widget(无论是 StatefulWidget 还是 StatelessWidget),必须加上 @FairPatch() 注解。在 counting 示例中,我们想把 MyHomePage这个 StatefulWidget 变为一个动态页面,所以,我们需要为其加上 @FairPatch() 注解:

()
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  
  final String title;
  
  
  State<MyHomePage> createState() => _MyHomePageState();
}

Tips:每一个 dart 文件里只能包含一个 @FairPatch。

step3.定义变量接收外部参数

我们强烈建议通过一个 Map 来传递参数到动态页面,所以需要定义一个 Map 类型的变量来接收外部参数。由于 Map 类型的 value 值类型不确定,所以直接定义一个 dynamic 的变量即可:

()
class MyHomePage extends StatefulWidget {
  // const MyHomePage({Key? key, required this.title}) : super(key: key);
  
  // final String title;
  
  MyHomePage({Key? key, this.fairProps}) : super(key: key);

  dynamic fairProps;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

通常习惯上,我们将变量名定义为 fairProps

step4.在 State 中定义与 JS 交互参数

_MyHomePageState中,需要定义一个参数用来与JS交互,并且需要使用@FairProps()进行标记。通常做法是也命名为 fairProps,当然,也可以自定义名称。

 ()
 var fairProps;

fairProps的初始化,需要在 initState()里进行:

  
  void initState() {
    super.initState();
    /// 需要将 widget.fairProps 赋值给 fairProps
    fairProps = widget.fairProps;
  }

如果在 build()方法里面的 UI Widget 需要使用到参数的话,统一通过 fairProps获取,例如:

String getTitle() {
    return fairProps['title'];
  }


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(getTitle()),
    ),
    
    ...
    
  );
}

完整代码参考如下:

class _MyHomePageState extends State<MyHomePage> {

  /// 定义与 JS 侧交互的参数,只支持 Map 类型的数据
  ///
  /// 需要用 @FairProps() 注解标记
  /// 变量名可以自定义,习惯上命名为 fairProps
  ()
  var fairProps;

  int _counter = 0;

  
  void initState() {
    super.initState();
    /// 需要将 widget.fairProps 赋值给 fairProps
    fairProps = widget.fairProps;
  }

  String getTitle() {
    return fairProps['title'];
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(getTitle()),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              // 暂不支持 style: Theme.of(context).textTheme.headline4,
              // 可替换成:
              style: TextStyle(fontSize: 40, color: Color(0xffeb4237), wordSpacing: 0),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Tips:style: Theme.of(context).textTheme.headline4这样的语法暂不支持,可以使用 style: TextStyle(fontSize: 40, color: Color(0xffeb4237), wordSpacing: 0) 代替。

step5.执行 build_runner 命令,编译生成下发产物

接下来,我们运行一个 build_runner 命令,触发 Fair 的 Compiler 开始编译工作,生成下发产物了。

运行命令如下:

flutter pub run build_runner build

生成成功:

image.png

产物的生成位置:项目根目录/build/fair

image.png

编译产物说明:

编译成功后,在 build/fair 目录下可以找到与 @FairPatch() 所在的 dart 文件同名的资源,一般以 lib 开头。主要的产物有以下几个:

  • .fair.bin 格式为 release 产物
  • .fair.json 格式为 debug 产物
  • .fair.js 格式为逻辑转换为JS后的产物
  • .fair.metadata 格式为元数据,标记了源码与产物的关联信息

.fair.json 主要是 debug 期间使用,因为 JSON 文件比较易读,便于排查错误。而 .fair.bin主要是在 release 期间使用,它是使用 FlatBuffers 工具生成的一种二进制文件,好处是不用反序列化,大大的提升了 Fair 解析、加载资源的速度。

其中,我们将 JSON 文件(或 bin 文件)和 JS 文件合称为 bundle 资源。

image.png 我们建议你以 bin 文件和 JS 文件作为最终的 bundle 资源,因为 bin 文件无需反序列化,可以提升 Fair 的加载效率。JSON 文件可以作为 debug 期间使用。

step6.使用 FairWidget 加载 bundle 资源

bundle 资源生成好以后,我们可以先本地测试一下,先将 bundle 资源拷贝到 assets 目录下,然后使用 FairWidget加载看效果(别忘了先在 yaml 中配置 assets 目录路径):

image.png

使用 FairWidget 时,有两个主要的参数,第一个参数是 path:bundle 资源的路径。

path 可以接受一个 assets 路径,如 'assets/bundle/lib_main.fair.json'。一般用来做本地调试的时候使用。

path 也可以接受一个手机本地磁盘的路径,注意是绝对路径。比如将 bundle 资源托管到自己公司服务器上,运行期间下载存储到手机磁盘后,以 bundle 文件的磁盘路径作为 path。

建议 Android 设备将 bundle 保存到 External Storage 目录,iOS 设备保存到 Application Documents 目录下

第二个参数是 data,data 是传递给动态页面的参数,data 是一个 Map<String, dynamic>结构的参数。

注意,传递给动态页面的数据,key 必须是 fairProps,不可以自定义,value 是一个 Map 类型的数据,需要进行 jsonEncode()操作,如:

data: {
/// 此处的 key 必须是 fairProps,不可以自定义
/// value 是一个 Map 类型的数据,最好是进行 jsonEncode() 操作
'fairProps': jsonEncode({'title': '你好'})
}

完整代码如下:

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        // home: MyHomePage(title: 'Flutter Demo Home Page'),

        /// FairWidget 是用来加载 bundle 资源的容器
        ///
        /// path 参数:需要加载的 bundle 资源文件路径
        /// data 参数:需要传递给动态页面的参数
        home: FairWidget(
            /// path 可以是 assets 目录下的 bundle 资源,也可以是手机存储
            /// 里的 bundle 资源,如果是手机存储里的 bundle 资源需要使用绝对路径
            path: 'assets/bundle/lib_main.fair.json',
            data: {
              /// 此处的 key 必须是 fairProps,不可以自定义
              /// value 是一个 Map 类型的数据,最好是进行 jsonEncode() 操作
              'fairProps': jsonEncode({'title': '你好'})
            }));
  }
}

然后,可以运行项目,效果如下:

Screenshot_20220524_163004_com.example.example.jpg

step7:将 bundle 资源托管到服务器上

本地调试没有问题后,就可以将 bundle 资源(bin 文件 + JS 文件)上传到自己公司服务器上。

注:目前 Fair 的热更新平台正在开发中,还没有上线,因此,上传服务、下载服务需要使用者自行开发。后续热更新平台上线后,可以直接使用我们的服务。

最后,贴一下改造后的 counting 例子的完整代码:

import 'dart:convert';

import 'package:fair/fair.dart';
import 'package:flutter/material.dart';

void main() {
  // runApp(MyApp());

  WidgetsFlutterBinding.ensureInitialized();

  FairApp.runApplication(
    _getApp(),
    plugins: {},
  );
}

dynamic _getApp() => FairApp(
  modules: {},
  delegate: {},
  child: MyApp(),
);

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        // home: MyHomePage(title: 'Flutter Demo Home Page'),

        /// FairWidget 是用来加载 bundle 资源的容器
        ///
        /// path 参数:需要加载的 bundle 资源文件路径
        /// data 参数:需要传递给动态页面的参数
        home: FairWidget(
            /// path 可以是 assets 目录下的 bundle 资源,也可以是手机存储
            /// 里的 bundle 资源,如果是手机存储里的 bundle 资源需要使用绝对路径
            path: 'assets/bundle/lib_main.fair.json',
            data: {
              /// 此处的 key 必须是 fairProps,不可以自定义
              /// value 是一个 Map 类型的数据,最好是进行 jsonEncode() 操作
              'fairProps': jsonEncode({'title': '你好'})
            }));
  }
}

()
class MyHomePage extends StatefulWidget {
  // const MyHomePage({Key? key, required this.title}) : super(key: key);

  // final String title;

  MyHomePage({Key? key, this.fairProps}) : super(key: key);

  // 通常习惯上,我们将变量名定义为 fairProps
  dynamic fairProps;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  /// 定义与 JS 侧交互的参数,只支持 Map 类型的数据
  ///
  /// 需要用 @FairProps() 注解标记
  /// 变量名可以自定义,习惯上命名为 fairProps
  ()
  var fairProps;

  int _counter = 0;

  
  void initState() {
    super.initState();
    /// 需要将 widget.fairProps 赋值给 fairProps
    fairProps = widget.fairProps;
  }

  String getTitle() {
    return fairProps['title'];
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(getTitle()),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              // 暂不支持 style: Theme.of(context).textTheme.headline4,
              // 可替换成:
              style: TextStyle(fontSize: 40, color: Color(0xffeb4237), wordSpacing: 0),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
上次编辑于:
贡献者: sunzhe03