Flutter动态表达式求值插件flutter_eval的使用

发布于 1周前 作者 zlyuanteng 来自 Flutter

Flutter动态表达式求值插件flutter_eval的使用

flutter_eval 是一个为 dart_eval 提供的 Flutter 桥接库,它是一个解决方案,可以实现代码推送、动态小部件和运行时 Flutter 代码评估。它可以用于通过空中更新热插拔应用程序的部分内容,从服务器动态加载用户界面,或基于用户输入(如计算器)进行代码评估。flutter_eval 支持所有平台,包括 iOS,并且基于 dart_eval 的自定义字节码解释器,因此非常快速。

插件信息

插件名称 版本信息
dart_eval pub package
flutter_eval pub package
eval_annotation pub package

实时示例

访问 EvalPad 查看实时示例。

注意事项

虽然 flutter_eval 支持大多数 Flutter 和 Dart 功能,但并非所有功能都受支持。现有的代码可能需要修改才能正常工作。您可以查看 这里 列出的支持的小部件和类,以及在 dart_eval Pub 页面 上列出的受支持的 Dart 特性。

快速入门:代码推送

首先,运行以下命令安装插件:

flutter pub add flutter_eval

在应用根目录下添加一个 HotSwapLoader 小部件,其中包含指向您将托管更新文件的位置的 URI:

class MyApp extends StatelessWidget {

  [@override](/user/override)
  Widget build(BuildContext context) {
    return HotSwapLoader(
        uri: 'https://mysite.com/app_update/version_xxx.evc',
        child: MaterialApp(
          ...
        ),
    );
  }
}

然后,在应用中添加 HotSwap 小部件以在每个位置动态更新:

class MyHomePage extends StatelessWidget {
  
  [@override](/user/override)
  Widget build(BuildContext context) {
    return HotSwap(
      id: '#myHomePage',
      args: [BuildContext.wrap(context)],
      childBuilder: (context) => Scaffold(
        ...
      ),
    );
  }
}

接下来,创建一个新的 Flutter 包,使用 flutter create --template=package 命令。这可以在您的应用文件夹内或单独创建。命名为适当的名称,例如 my_app_hot_update。我们将从现在起将其称为“热更新包”。

转到 flutter_eval发布页面,找到与您使用的 flutter_eval 版本对应的发布版本。在 Assets 下,下载 flutter_eval.json。(或者点击 这里 下载最新版本。)

在热更新包的根目录下,创建一个名为 .dart_eval 的文件夹和一个子文件夹 bindings。将下载的 flutter_eval.json 文件放入此文件夹中。

您的项目结构应该如下所示:

├── .dart_eval
│   └── bindings
│       └── flutter_eval.json.
├── pubspec.yaml
└── lib
    └── hot_update.dart

在热更新包的根目录下运行以下命令:

flutter pub add eval_annotation

删除 main.dart 中的所有代码,包括 main() 函数。对于您希望更新的每个 HotSwap 小部件(可以更新全部、部分或仅一个),创建一个带有 @RuntimeOverride 注解的顶级函数,引用其 ID:

@RuntimeOverride('#myHomePage')
Widget myHomePageUpdate(BuildContext context) {
  return Scaffold(
    ...
  )
}

最后,我们需要安装 dart_eval CLI:

dart pub global activate dart_eval

安装后,可以运行以下命令:

dart_eval compile -o version_xxx.evc

如果步骤正确执行,您应该会在控制台输出中看到以下内容:

Found binding file: .dart_eval\bindings\flutter_eval.json

以及

Compiled $x characters Dart to $y bytes EVC in $z ms: version_xxx.evc

生成的 version_xxx.evc 文件将在项目的根目录中,现在您可以将其上传到之前提到的服务器路径。

如果您运行该应用,您应该会看到 HotSwap 小部件的内容被替换为其相应 @RuntimeOverride 函数的内容。

HotSwap 小部件可以选择指定一种策略来加载/应用更新,即 immediatecachecacheApplyOnRestart。默认情况下,HotSwapLoader 在调试/配置模式下使用 immediate,在发布模式下使用 cacheApplyOnRestart。您还可以通过 loading 参数指定加载缓存时要显示的占位小部件。

完整的代码推送示例可查看 此处 和其子文件夹 此处

快速入门:动态执行和服务器驱动的UI

要创建一个简单的动态无状态小部件,可以在 build() 方法内部或作为子参数添加并修改以下内容:

return EvalWidget(packages: {
  'example': {
    'main.dart': '''
    import 'package:flutter/material.dart';
    
    class MyWidget extends StatelessWidget {
      MyWidget(this.name);
      final String name;

      [@override](/user/override)
      Widget build(BuildContext context) {
        return Padding(
          padding: EdgeInsets.all(5.0),
          child: Column(children: [
              Container(
                color: Colors.red,
                child: Text('The name is ' + name)
              )
            ],
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
          )
        );
      }
    }''',
    assetPath: 'assets/program.evc',
    library: 'package:example/main.dart',
    function: 'MyWidget.',
    args: [String('Example name')]
  }
});

内部地,EvalWidget 会自动切换两种模式:

  • 调试模式 下,它将动态编译提供的 Dart 代码为 EVC 字节码,保存到由 assetPath 确定的文件,并在 dart_eval VM 中运行。这较慢,但允许您在开发过程中进行热重启。
  • 发布模式 下,它将忽略提供的 Dart 代码,尝试从 assetPath 或由可选的 uri 参数指定的自定义文件、资产或网络 URL 加载 EVC 字节码。这在运行时提供了高性能,同时仍然允许您通过提供新的 EVC 文件或 URL 来动态交换代码。

如果您从网络加载 EVC 字节码,可以使用 loading 参数指定可选的加载小部件。

flutter_eval 包含两个其他辅助小部件,用于不同的用例:

  • CompilerWidget 将始终编译并运行提供的 Dart 代码,无论应用程序是在调试还是发布模式下都不会尝试加载 EVC 字节码。这特别适合于计算器、学习编码平台或用户脚本化自动化工具等应用。尽管这种方式较慢,但 dart_eval 编译器非常快,通常会在约 0.1 秒内编译简单的 Flutter 程序。
  • RuntimeWidget 将始终加载 EVC 字节码,并不提供指定 Dart 代码的参数。如果使用 CLI 编译 - 请参见下方。

调用函数和传递参数

要使用默认构造函数实例化类,请在类名后面附加一个点号 .

当外部调用 dart_eval 函数或构造函数时,必须按顺序指定所有参数,即使这些参数是可选或命名的参数。使用 null 表示缺少参数(而 $null() 表示空值)。

例如,对于以下类:

class MyApp extends SomeWidget {
  MyApp(this.name, {Key? key, Color? color}) : super(key: key, color: color);

  final String name;
}

您可以在 RuntimeWidget 中使用以下方式实例化它:

return RuntimeWidget(
  uri: Uri.parse('asset:assets/program.evc'),
  library: 'package:example/main.dart',
  function: 'MyApp.',
  args: [String('Jessica'), null, null]
);

您也可以使用 $Function 传递回调。

安全性和权限

flutter_eval 继承了 dart_eval 的安全执行模型,该模型默认限制对文件系统、网络和其他敏感 API 的访问。flutter_eval 还扩展了对 MethodChannel 及相关 API 的安全控制。要允许访问 MethodChannel,请在 EvalWidgetCompilerWidgetRuntimeWidgetpermissions 参数中添加 MethodChannelPermission

支持的小部件和类

目前支持的小部件和类包括:

  • Widget, StatelessWidget, StatefulWidget, State;
  • ChangeNotifier, Key, BuildContext;
  • WidgetsApp, Container, Column, Row, Center;
  • Alignment, Align, AspectRatio, Baseline;
  • Radius, BorderRadius, BorderRadiusGeometry, Clip, ClipRRect;
  • SizedBox, FittedBox, FractionallySizedBox, ColoredBox;
  • Stack, Positioned, StackFit;
  • Padding, EdgeInsetsGeometry, EdgeInsets, Axis, Size;
  • Offset, Velocity;
  • MainAxisAlignment, MainAxisSize, CrossAxisAlignment;
  • AlignmentGeometry, Alignment, Constraints, BoxConstraints;
  • Color, ColorSwatch, Colors, FontWeight, FontStyle;
  • MaterialApp, MaterialColor, MaterialAccentColor;
  • Theme, ThemeData, TextTheme, Directionality;
  • Decoration, BoxDecoration, BoxBorder, Border, BorderSide;
  • IconData, Icons, Icon;
  • Curve, Curves, SawTooth, Interval, Threshold, Cubic;
  • Text, TextStyle, TextEditingController, TextField;
  • TextDirection, VerticalDirection, TextBaseline
  • Scaffold, ScaffoldMessenger, AppBar, SnackBar, FloatingActionButton;
  • InkWell, TextButton, ElevatedButton, IconButton;
  • Card, Drawer, ListView, ListTile, Spacer;
  • Image, ImageProvider, NetworkImage, MemoryImage;
  • Navigator, NavigatorState, Builder;
  • PointerDeviceKind, HitTestBehavior;
  • GestureDetector, TapDownDetails, TapUpDetails;
  • LongPressStartDetails, LongPressMoveUpdateDetails, LongPressEndDetails;
  • DragStartDetails, DragUpdateDetails, DragEndDetails, DragDownDetails;

请注意,其中一些仅具有部分支持。

应用大小测量

应用 初始 APK 大小 使用 EvalWidget 后的大小
Flutter Counter 16.5 MB 17.9 MB (+ 1.4 MB)
Flutter Gallery 110.2 MB 110.6 MB (+ 0.4 MB)

这些测量数据是在 flutter_eval v0.4.5 更新后的。它们不包括 EVC 字节码文件的大小,通常为 20-100KB(或压缩后 6-30KB),并且可能在安装后从服务器下载而不是与应用一起打包。

请注意,这些测量数据是为包含多个架构的生成组合 APK。从 Play 商店下载的 APK 大小大约是其一半,并且增加量也会减少。

高级用法

使用 flutter_eval 需要两个主要步骤:将 Dart 代码编译为 EVC 字节码,以及执行生成的 EVC 字节码。由于您不能期望所有现有的 Flutter/Dart 代码都能与 flutter_eval 兼容,建议在开发期间运行这两个步骤:

class ExampleState extends State<Example> {
  late Runtime runtime;

  [@override](/user/override)
  void initState() {
    super.initState();

    final compiler = Compiler();
    compiler.addPlugin(flutterEvalPlugin);

    final program = compiler.compile({
      'example': { 'main.dart': '''
          import 'package:flutter/material.dart';
          
          class HomePage extends StatelessWidget {
            HomePage(this.number);
            final int number;
            
            [@override](/user/override)
            Widget build(BuildContext context) {
              return Padding(
                padding: EdgeInsets.all(2.3 * 5),
                child: Container(
                  color: Colors.green,
                  child: Text('Current amount: ' + number.toString())
                )
              );
            }
          }
        ''' }
    });

    final file = File('out.evc');
    file.writeAsBytesSync(program.write());
    print('Wrote out.evc to: ' + file.absolute.uri);
    
    runtime = Runtime.ofProgram(program);
    runtime.addPlugin(flutterEvalPlugin);
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return (runtime.executeLib('package:example/main.dart', 'HomePage.', [int(55)]) as Value).$value;
  }
}

有了这个设置,您可以快速查看代码中的任何错误。但是,我们也持续将 EVC 字节码写入文件 out.evc。此文件包含从 Dart 源生成的编译字节码,当发布应用到生产环境时,这就是您所需的一切。在生产环境中运行编译器可能会导致应用运行速度变慢,因此强烈建议在生成后使用输出。EVC 字节码是平台无关的,因此您可以使用 Flutter 桌面生成 out.evc 文件并在 Flutter 移动应用中使用它。

生成后,您可以像这样在应用中使用它:

import 'package:flutter/services.dart' show rootBundle;

class ExampleState extends State<Example> {
  Runtime? runtime;

  [@override](/user/override)
  void initState() {
    super.initState();
    
    rootBundle.load('assets/out.evc').then((bytecode) => setState(() {
      runtime = Runtime(ByteData.sublistView(bytecode));
      runtime.addPlugin(flutterEvalPlugin);
    }));
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    if (runtime == null) return CircularProgressIndicator();
    return (runtime.executeLib('package:example/main.dart', 'HomePage.', [int(55)]) as Value).$value;
  }
}

更多关于Flutter动态表达式求值插件flutter_eval的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter动态表达式求值插件flutter_eval的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用flutter_eval插件来进行动态表达式求值的示例代码。这个插件允许你在运行时解析和执行数学表达式。

1. 添加依赖

首先,你需要在pubspec.yaml文件中添加flutter_eval依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_eval: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

2. 导入插件

在你的Dart文件中导入flutter_eval插件:

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

3. 使用插件进行表达式求值

以下是一个简单的示例,展示如何使用flutter_eval来求值一个简单的数学表达式:

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Eval Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: EvalDemo(),
    );
  }
}

class EvalDemo extends StatefulWidget {
  @override
  _EvalDemoState createState() => _EvalDemoState();
}

class _EvalDemoState extends State<EvalDemo> {
  final TextEditingController _controller = TextEditingController();
  String _result = '';

  void _evaluateExpression() {
    final expression = _controller.text;
    try {
      final context = EvalContext();
      final result = context.eval(expression);
      setState(() {
        _result = result.toString();
      });
    } catch (e) {
      setState(() {
        _result = 'Error: $e';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Eval Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: _controller,
              decoration: InputDecoration(
                border: OutlineInputBorder(),
                labelText: 'Enter Expression',
              ),
              keyboardType: TextInputType.multiline,
              maxLines: 5,
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: _evaluateExpression,
              child: Text('Evaluate'),
            ),
            SizedBox(height: 16),
            Text(
              'Result: $_result',
              style: TextStyle(fontSize: 20),
            ),
          ],
        ),
      ),
    );
  }
}

解释

  1. 添加依赖:在pubspec.yaml中添加flutter_eval依赖。
  2. 导入插件:在需要使用flutter_eval的Dart文件中导入它。
  3. 构建UI:创建一个简单的UI,包含一个文本字段用于输入表达式,一个按钮用于触发求值操作,以及一个文本标签用于显示结果。
  4. 求值函数:在按钮的点击事件处理函数中,使用EvalContext来解析和执行输入的表达式。如果表达式有效,将结果显示在文本标签中;如果无效,则显示错误信息。

注意事项

  • flutter_eval支持多种数学函数和操作符,但请确保输入的表达式符合其语法规则。
  • 在生产环境中,务必对用户输入进行验证,以防止潜在的安全问题(如代码注入)。

希望这个示例对你有所帮助!如果有其他问题,请随时提问。

回到顶部