Flutter响应式计算插件computed的使用

Flutter响应式计算插件computed的使用

简介

Computed 是一个高性能且灵活的反应式编程框架,适用于Dart。如果你正在使用Flutter,建议查看 Computed Flutter,它专门为Flutter进行了优化。

Computed 的主要特点包括:

  • 集成标准数据源:支持 Future 和 Stream(Dart),以及 Listenable 和 ValueListenable(Flutter)。
  • 扩展性:可以扩展以支持新的数据源和接收器。
  • 自动依赖跟踪:自动发现和跟踪计算的依赖关系。
  • 自动重新计算:在需要时重新计算。
  • 结果缓存:缓存计算结果。
  • 延迟计算:尽可能延迟计算。
  • 拓扑顺序运行:在外部事件发生时按拓扑顺序运行计算。
  • 避免重复计算:除非上游发生变化,否则不重新运行计算。

基本用法

假设你有一个数据源,比如一个表示一系列外部事件的 Stream:

Stream<int> s;

假设你有一个数据库,希望将状态持久化到其中:

FictionaryDatabase db;

假设你的业务逻辑是将接收到的数字乘以二,并将其写入数据库。使用 Computed 可以这样实现:

final sub = $(() => s.use * 2)
  .listen(db.write);

这就是全部了。Computed 会负责重新运行计算并在需要时调用监听器。注意,你不需要指定依赖列表,Computed 会自动发现它们。你甚至不需要在代码中显式地管理可变状态。

要取消监听器,可以使用 .cancel():

sub.cancel();

你也可以有使用其他计算结果的计算:

final cPlus1 = $(() => c.use + 1);

较大的示例

假设你有两个数据源,一个是整数流:

Stream<int> threshold;

另一个是整数列表流:

Stream<List<int>> items;

假设你需要实现一些逻辑,即根据第一个流中的阈值过滤第二个流中的项,并将结果保存到数据库中。以下是使用命令式方法的实现:

首先定义状态:

int? currentThreshold;
var currentUnfilteredItems = <int>[];

然后设置监听器:

threshold.listen((value) => {
    if (value == currentThreshold) return;
    currentThreshold = value;
    updateDB();
});

items.listen((value) => {
    currentUnfilteredItems = value;
    updateDB();
});

最后定义状态计算逻辑:

void updateDB() {
    if (currentThreshold == null) return;
    final filteredItems = currentUnfilteredItems
        .where((x) => x > currentThreshold)
        .toList();
    db.write(filteredItems);
}

使用 Computed 实现相同的功能:

Computed(() {
    final currentThreshold = threshold.use;
    return items.use
        .where((x) => x > currentThreshold)
        .toList();
}, assertIdempotent: false).listen(db.write);

注意使用 assertIdempotent: false,因为 List 在其等式运算符周围没有值语义。一般来说,computed_collections 是表达反应式集合的更好解决方案。

计算配置

memoized

如果设置为 true(默认值),Computed 会“缓存”此计算的值,如果此计算返回的值或抛出的异常与上一次相同,则不会通知下游/监听器。

assertIdempotent

如果设置为 true(默认值),Computed 会在调试模式下初始运行两次计算并检查两次运行是否返回相等的值或抛出相等的异常。如果不相等,它会断言。

async

如果设置为 false(默认值),Computed 会在不允许异步操作的 Zone 中运行此计算。设置为 true 意味着 assertIdempotent==false。

dispose

如果设置,此回调将在以下情况下被调用:

  • 值发生变化,
  • 从生成值变为抛出异常,
  • 失去所有监听器和非弱下游计算, 如果之前有值的话。

onCancel

如果设置,此回调将在计算失去所有监听器和非弱下游计算时被调用。在 dispose 之后调用。

效果

效果允许你定义具有副作用的计算。像 .listen 一样,效果最终会触发所使用计算的数据源的计算图。效果特别有用,如果你希望根据多个数据源或计算定义副作用:

Stream<PageType> activePage;
Stream<bool> isDarkMode;

final sub = Computed.effect(() => sendAnalytics(activePage.use, isDarkMode.use));

像监听器一样,效果也可以通过 .cancel() 取消:

sub.cancel();

查看过去

.use 返回数据源或计算的当前值,那么如何在不使用应用程序代码中的可变状态的情况下查看过去呢?.prev 允许你获取给定数据源或计算在当前计算上次运行时的最后一个值。

这是一个简单的示例,计算数据源每次生成值时的新旧值之间的差异:

final c = $(() {
    s.use; // 确保它有值
    late int res;
    s.react((val) => res = val - s.prevOr(0));
    return res;
}, memoized: false);

注意这里使用了 .react。.react 标记当前计算在数据源生成的所有值时重新计算,即使连续生成的值比较 ==。.react 将在数据源生成新值/错误时运行给定的函数。作为一个经验法则,对于表示事件序列而不是状态的数据源,你应该使用 .react 而不是 .use。.prevOr 是一个方便的快捷方式,如果数据源在当前计算上次通知其监听器或其他依赖于它的计算时没有值,则返回给定的回退值,而不是抛出 NoValueException。memoized: false 防止计算的结果被缓存,因为我们希望下游计算和监听器即使差异没有变化也会被通知。

你还可以创建时间累加器:

final sum = Computed<int>.withPrev((prev) {
    var res = prev;
    s.react((val) => res += val);
    return res;
}, initialPrev: 0);

计算查询

你的应用程序可能需要运行带有计算状态作为参数的查询。你可以使用“异步”计算和 unwrap 来实现:

class FictionaryDatabase {
    Future<List<Object>> filterByCategory(int category, bool includeDeleted);
}

Stream<int> category; // 假设连接到UI
Stream<bool> includeDeleted; // 假设连接到UI
FictionaryDatabase db; // 假设连接到数据库

final query = Computed.async(() =>
  db.filterByCategory(category.use, includeDeleted.use)).unwrap;

使用 Computed.async 禁用一些对启动异步操作的计算不适用的检查。unwrap 返回一个表示由应用于它的计算返回的最后一个异步操作生成的最后一个值的计算。在这个例子中,它将计算类型从 Computed<Future<int>> 转换为 Computed<int>。如果数据库返回 Stream 而不是 Future,unwrap 也适用。当然,其他计算可以使用计算查询的结果,因为它本身就是一个计算。

流工具

Computed 包含一组在反应式环境中可能有用的最小 Stream 工具。你可以在 lib/utils 找到它们。

计算缓存

你可能会发现自己需要创建参数化的计算。简单的方法是每次创建一个新的计算,但这会导致多个具有相同参数的计算创建时的重复计算。在这种情况下,你可以使用 ComputationCache 来“统一”具有相同参数的计算。ComputationCache 确保每个缓存键在同一时间最多只有一个活动计算,无论创建了多少个计算。当然,它通过在必要时通知所有创建的计算的监听器和下游计算来保留反应式语义。

高级用法

这一部分描述了 Computed 的高级功能。你很少需要它们。

机会性使用计算

你可能会发现自己处于多种方式计算值的情况,其中一种方式是昂贵的但计算多个相关值,另一种方式是计算更便宜但更集中。在这种情况下,你可以使用 useWeak。它允许计算在不触发它们被计算的情况下反应式地使用其他计算,如果它们没有监听器或非弱下游计算。

调用 useWeak 会在当前计算中弱订阅目标计算。如果目标计算有监听器或非弱下游,useWeak 将返回其值。否则,它将抛出 NoStrongUserException。如果目标计算更改值或失去所有监听器或非弱下游,它将调度当前计算重新运行,但如果它获得任何监听器或非弱下游,则不会。

自定义下游

默认情况下,Computed 会在计算更改值时调度下游计算重新运行。如果这样做计算效率不高,你可以自定义应调度重新运行的下游。你可以在 computed_collections 仓库中找到一个示例。

请注意,使用此功能需要你依赖 Computed 的内部实现,这限制了你只能使用个别补丁版本。谨慎使用,因为 Computed 没有防止使用自定义下游破坏反应式一致性的防护措施。

常见问题

为什么我收到 Computed expressions must be purely functional. Please use listeners for side effects.?

在调试模式下,Computed 会两次运行给定的计算并检查两次调用是否返回相同的值。如果不成立,它会抛出此断言。可能的原因包括在计算中变异和使用可变值,或者返回没有实现深层比较的类型,如 List 或 Set。考虑返回一个为其等式运算符实现值语义的类型,或者使用 assertIdempotent: false。如果你的计算启动了一个异步过程并返回 Stream 或 Future,请考虑使用 Computed.async,如 计算查询 中所述。

常见陷阱

不要在计算中使用可变值

特别是如果它们用于保护 .use 表达式的条件。例如:

Stream<int> value;
var b = false;

final c = $(() => b ? value.use : 42);

这可能导致 Computed 停止跟踪 value,从而破坏计算的反应性。

使用异步模式启动异步操作的计算

这将禁用一些对这种计算没有意义的检查。

不要在一个计算中使用由该计算创建的 Future 或 Stream

这可能会导致计算运行、创建新的数据源并在生成值时再次运行之间的无限循环。

不要在一个计算中使用由该计算创建的计算

如果外层计算每次都重新创建内层计算,这可能是低效的。相反,考虑在创建外层计算之前创建内层计算,并将其作为闭包的一部分捕获。如果你需要反应式值来构建内层计算,考虑在一个专用的中间计算中构建它,使用 assertIdempotent: false 并在外层计算中 unwrap 中间计算。

Future 从计算返回时不等待

至少为了缓存目的。DAG 传递永远不会等待,即使计算返回 Future。结果将原样传递给下游计算。如果你想返回由计算返回的异步操作生成的值,请参阅 .unwrap。

不要忘记使用 .use 或 .react 你的数据源

如果计算在数据源上没有调用这两个中的任何一个就返回,Computed 会假设它不依赖于该数据源。

注意 .prev 不订阅

它也是针对当前运行的计算的“主观”。不同的函数在同一数据源或计算上可以有不同的 .prev。

.react 不是 .listen

.react 只能在计算内部使用,.listen 只能在计算外部使用。 .react 回调除了当前计算的本地范围外不应有副作用。.listen 回调是发生副作用的地方。 .listen 保留对给定回调的引用并在稍后的时间点调用它。.react 要么在返回前调用给定的函数,要么根本不调用。

示例代码

import 'dart:async';
import 'package:built_collection/built_collection.dart';
import 'package:computed/computed.dart';

void main() async {
  final controller = StreamController<BuiltList<int>>(sync: true); // 使用同步控制器以便调试更容易
  final source = controller.stream.asBroadcastStream();

  final anyNegative = $(() => source.use.any((element) => element < 0));

  final maybeReversed = $(() => anyNegative.use ? source.use.reversed.toBuiltList() : source.use);

  final append0 = $(() {
    return maybeReversed.use.rebuild((p0) => p0.add(0));
  });

  Computed.effect(() => print(append0.use));

  // 忽略未使用的局部变量
  final unused = $(() {
    while (true) {
      print("Never prints, this computation is never used.");
    }
  });

  controller.add([1, 2, -3, 4].toBuiltList()); // 输出 [4, -3, 2, 1, 0]
  controller.add([1, 2, -3, -4].toBuiltList()); // 输出 [-4, -3, 2, 1, 0]
  controller.add([4, 5, 6].toBuiltList()); // 输出 [4, 5, 6, 0]
  controller.add([4, 5, 6].toBuiltList()); // 相同结果:不再输出
}

这个示例展示了如何使用 Computed 进行反应式计算和效果处理。希望这些信息对你有所帮助!


更多关于Flutter响应式计算插件computed的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter响应式计算插件computed的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,computed 插件可以帮助你创建响应式计算属性,这些属性会基于其他状态的变化自动更新。这对于构建复杂的状态管理逻辑非常有用。下面是一个简单的示例,展示如何在Flutter项目中使用 computed 插件。

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

dependencies:
  flutter:
    sdk: flutter
  computed: ^0.4.0  # 请检查最新版本号

然后运行 flutter pub get 来获取依赖。

接下来,我们可以创建一个简单的Flutter应用,演示如何使用 computed 插件。

示例代码

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // 定义可变状态
  var _count = Var<int>(0);

  // 创建一个 computed 属性
  var _doubleCount = computed(() => _count.value * 2);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Computed Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Count: ${_count.value}',
              style: TextStyle(fontSize: 24),
            ),
            Text(
              'Double Count: ${_doubleCount.value}',
              style: TextStyle(fontSize: 24, color: Colors.blue),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // 更新状态
                setState(() {
                  _count.update((value) => value + 1);
                });
              },
              child: Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

代码解释

  1. 定义可变状态:使用 Var<int> 创建一个可变状态 _count,初始值为 0。
  2. 创建 computed 属性:使用 computed 函数创建一个计算属性 _doubleCount,该属性基于 _count 的值计算得出,即 _count.value * 2。
  3. 在构建方法中使用:在 build 方法中,使用 _count.value 和 _doubleCount.value 来显示当前的计数和它的两倍值。
  4. 更新状态:在按钮的 onPressed 回调中,使用 setState 方法来更新 _count 的值。当 _count 更新时,_doubleCount 会自动重新计算。

通过这种方式,你可以使用 computed 插件来创建响应式计算属性,从而简化状态管理逻辑,并避免手动更新依赖于其他状态的属性。

回到顶部