Flutter动态计算集合插件computed_collections的使用

Flutter 动态计算集合插件 computed_collections 的使用

computed_collections 是一个用于函数式响应编程的高性能且灵活的库。它基于 computed 构建,并利用 fast_immutable_collections 提供高性能且强大的不可变集合。

目录

表的内容

一个示例

假设你想在你的用户界面中显示一组对象。你从数据库或服务器获取这些对象。然而,你只想显示那些最近被修改的对象。

假设你有以下代码:

Stream<Map<ObjectId, MyObject>> objectsUnfiltered = ...;
void displayInUI(List<MyObject> objects) {
  ...
}
bool isRecent(MyObject object) {
  ...
}

以下是使用命令式方法处理这个问题的方式:

objectsUnfiltered
  .listen((o) => displayInUI(o.values.where((oo) => isRecent(oo)).toList()));

这很简单,但你看到问题了吗?监听器会为每次更改过滤所有的对象。如果你的对象数量不多,或者你希望运行的反应逻辑计算量不大,那么这种方法可能不会有问题,但在大型和复杂的应用程序中显然无法扩展。

为了缓解这个问题,你可以首先识别未过滤对象集如何发生变化,并仅对映射的相关部分运行反应逻辑:

({
  Map<ObjectId, MyObject> unfiltered,
  Map<ObjectId, MyObject> filtered
})? lastObjects;

objectsUnfiltered.listen((objs) {
  switch (lastObjects) {
    case var last?:
      final changed =
          last.unfiltered.entries.where((e) => e.value != objs[e.key]);
      final addRemove = groupBy(changed, (e) => isRecent(e.value));
      last.filtered.addEntries(addRemove[true] ?? []);
      for (var e in (addRemove[false] ?? [])) {
        last.filtered.remove(e.key);
      }
      lastObjects = (filtered: last.filtered, unfiltered: objs);
      displayInUI(last.filtered.values.toList());
    case null:
      final filtered =
          Map.fromEntries(objs.entries.where((e) => isRecent(e.value)));
      lastObjects = (filtered: filtered, unfiltered: objs);
      displayInUI(filtered.values.toList());
  }
});

这确实有点复杂。我们不得不维护一对映射作为我们的状态:一个表示最后的未过滤对象集,另一个表示过滤后的对象集。对于流中的每次更改,我们需要计算上游的差异(如果存在以前的状态),并相应地更新状态和用户界面。

现在,让我们看看如何使用 computed_collections 实现这样的反应性集合:

final unfiltered =
      ComputedMap.fromSnapshotStream($(() => objectsUnfiltered.use.lock));
final filtered = unfiltered.removeWhere((k, v) => !isRecent(v));
filtered.snapshot.listen((s) => displayInUI(s.values.toList()));

这是全部代码。fromSnapshotStream 内部处理我们在命令式示例中手动进行的差异比较,并构造一个 变化流.removeWhere 订阅这个变化流以增量方式维护过滤后的映射。最后,我们使用 filtered.snapshot,这是一个表示过滤后映射快照的 computed 计算,我们可以在此附加一个监听器以更新用户界面。在这种情况下,我们需要将从流中获得的映射锁定为一个快速不可变映射,因为 computed_collections 专门操作于它们上,但这可以合理地认为不会损害渐近复杂度。

当然,最好摄取变化流而不是快照流,这样我们就可以避免在第一个地方对连续快照进行差异比较。这将带来…

摄取外部变化流

computed_collection 基于变化流构建。一个反应性映射,用接口 ComputedMap 表示,内部有一个快照和一个变化流。操作符,如 .map, .join.removeWhere 使用并转换这些流。如果你有一个变化流计算,你可以使用工厂函数 ComputedMap.fromChangeStream

Computed<ChangeEvent<ObjectId, MyObject>> changes = ...;
final cmap = ComputedMap.fromChangeStream(changes); // 类型为 `ComputedMap<ObjectId, MyObject>`

ChangeEvent 代表在映射上进行的一组更改。请注意,你可能需要编写一些“粘合”代码来将使用外部格式的变化流转换为 computed_collections 使用的格式。这应该不难与 computed 一起完成。

状态化变化流

你可能需要将外部变化流格式转换为状态化。你可以通过捕获在变化流计算闭包中的内部子计算来实现这一点。但如果所需的状态是你要构建的集合的快照呢?一个简单的用例是监听流并将每个发布的键的值增加一。你可以使用 ComputedMap.fromChangeStreamWithPrev 来实现这一点。类似于 computed<a href="https://pub.dev/documentation/computed/latest/computed/Computed/Computed.withPrev.html"><code>Computed.withPrev</code></a>,它将集合的前一个快照传递给变化流计算:

final s = StreamController<int>(sync: true);
final stream = s.stream;
final m = ComputedMap.fromChangeStreamWithPrev((prev){
  final c = KeyChanges(<int, ChangeRecord<int>>{}.lock);
  stream.react((idx) => c[idx] = ChangeRecordValue(prev[idx] ?? 0 + 1));
  return c;
});

m.snapshot.listen(print);

s.add(0); // 打印 {0:1}
s.add(0); // 打印 {0:2}
s.add(1); // 打印 {0:2, 1:1}

常量反应映射

你可以使用 ComputedMap.fromIMap 定义一个常量反应映射。然后它就不那么反应了。

final m1 = ComputedMap.fromIMap({1:2, 2:3}.lock);
final m2 = m1.mapValues((k, v) => v+1); // 结果是 {1:3, 2:4}
m2.snapshot.listen(print); // 打印 {1:3, 2:4}

具有键间依赖性的映射

你可以使用带有 *Computed 后缀的操作符来定义具有任意反应性依赖关系的转换。特别是,一个键的转换可以依赖于另一个键转换的结果。例如,以下程序声明式地计算 $[1,16]$ 范围内的整数在 Collatz 猜想中减少到 1 需要多少步:

final m1 = ComputedMap.fromIMap(
    IMap.fromEntries(List.generate(200, (i) => MapEntry(i, 0))));
late final ComputedMap<int, int> m2;
m2 = m1.mapValuesComputed((k, v) => k <= 1
    ? $(() => 0)
    : $(() => m2[((k % 2) == 0 ? k ~/ 2 : (k * 3 + 1))].use! + 1));
final m3 = m1
    .removeWhere((k, v) => k == 0 || k > 16)
    .mapValuesComputed((k, v) => m2[k]);
m3.snapshot.listen(print);

这可能不是最高效的实现,但请注意,它在渐进意义上是优化的,因为它使用了记忆化。它还展示了键局部查询的能力,因为计算 m1 键范围内的所有整数的 Collatz 序列($[1, 199]$)需要访问大于 199 的整数。

example/collatz.dart 中,你可以找到一个更高级的实现,它在内存复杂度上也是渐进最优的。

操作符索引

以下是关于反应式映射的操作符及其高阶描述列表。

操作符 描述
.add 反应式地向反应式映射添加给定的键值对。
.addAll 反应式地将给定的 IMap 添加到反应式映射中。
.addAllComputed 反应式地将给定的反应式映射添加到反应式映射中。
.cast 反应式地转换反应式映射中的条目。
.containsKey 返回一个表示反应式映射是否包含给定键的计算。
.containsValue 返回一个表示反应式映射是否包含给定值的计算。
.map 反应式地通过给定的同步函数映射反应式映射中的每个条目。
.mapComputed 反应式地通过给定的反应式转换计算映射反应式映射中的每个条目。
.mapValues 反应式地通过给定的同步函数映射反应式映射中的所有值。
.mapValuesComputed 反应式地通过给定的反应式转换计算映射反应式映射中的所有值。
.putIfAbsent 如果给定的键不存在,则反应式地将给定的键添加到反应式映射中。
.remove 反应式地从反应式映射中移除给定的键。
.removeWhere 反应式地从反应式映射中移除满足给定同步条件的所有条目。
.removeWhereComputed 反应式地从反应式映射中移除满足给定反应式条件计算的所有条目。
.update 反应式地通过给定的同步函数转换反应式映射中的给定键。
.updateAll <code>.mapValues</code> 的一种特殊情况,其中输入和输出类型相同。
.updateAllComputed <code>.mapValuesComputed</code> 的一种特殊情况,其中输入和输出类型相同。
.groupBy 反应式地通过给定的同步函数对反应式映射进行分组。
.groupByComputed 反应式地通过给定的反应式分组计算对反应式映射进行分组。
.join 计算一对反应式映射的反应式内连接。
.lookup 计算一对反应式映射的反应式左连接。
.cartesianProduct 计算一对反应式映射的反应式笛卡尔积。
.flat 反应式地展平反应式映射中的反应式映射。

属性索引

以下是关于反应式映射的属性及其高阶描述列表。

属性 描述
.changes 该映射的变化流计算。
.snapshot 该映射的快照计算。
.operator[] 该映射的给定键的计算表示。
.isEmpty 该映射的空性计算表示。
.isNotEmpty <code>.isEmpty</code> 的相反情况。
.length 该映射的长度计算表示。

工厂索引

以下是创建 ComputedMap 的方法列表。

工厂 描述
.fromChangeStream 跟踪给定的变化流。初始化为空映射。
.fromSnapshotStream 跟踪给定的快照流。内部通过差异连续快照创建变化流。
.fromIMap 与给定的 IMap 相等的常量映射。
.fromChangeStreamWithPrev 类似于 <code>.fromChangeStream</code>,但允许变化流计算依赖于映射的快照。
.fromPiecewise <code>.fromIMap</code> 的泛化,根据给定的键域上的反应性依赖关系创建计算映射。
.fromPiecewiseComputed 类似于 <code>.fromPiecewise</code>,但值由反应性计算定义。

示例代码

import 'dart:async';

import 'package:computed/computed.dart';
import 'package:computed_collections/change_event.dart';
import 'package:computed_collections/computedmap.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';

void main() {
  final s = StreamController<ChangeEvent<int, int>>(sync: true);
  final stream = s.stream;

  final m = ComputedMap.fromChangeStream($(() => stream.use))
      .removeWhere((key, value) => key % 2 == 1)
      .updateAll((key, value) => value + 1);

  final sub = Computed.effect(() => print(m.snapshot.use));

  // 打印 {}

  s.add(ChangeEventReplace({0: 1, 1: 2, 2: 3}.lock));
  // 打印 {0:2, 2:4}

  s.add(
      KeyChanges({0: ChangeRecordValue(0), 2: ChangeRecordDelete<int>()}.lock));
  // 打印 {0: 1}

  sub.cancel();
}

更多关于Flutter动态计算集合插件computed_collections的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter动态计算集合插件computed_collections的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


computed_collections 是一个用于 Flutter 的插件,它允许你动态计算集合中的数据,并在集合发生变化时自动更新计算结果。这对于处理需要根据集合中的数据动态生成结果的场景非常有用,比如过滤、排序、统计等操作。

安装插件

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

dependencies:
  flutter:
    sdk: flutter
  computed_collections: ^1.0.0  # 请使用最新版本

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

基本用法

computed_collections 提供了一个 ComputedList 类,它可以监听一个源列表的变化,并根据源列表动态生成一个新的列表。

示例:过滤列表

假设你有一个包含数字的列表,你想要动态过滤出所有大于 5 的数字:

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

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

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Computed Collections Example')),
        body: FilterListExample(),
      ),
    );
  }
}

class FilterListExample extends StatefulWidget {
  [@override](/user/override)
  _FilterListExampleState createState() => _FilterListExampleState();
}

class _FilterListExampleState extends State<FilterListExample> {
  List<int> sourceList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  late ComputedList<int> filteredList;

  [@override](/user/override)
  void initState() {
    super.initState();
    filteredList = ComputedList<int>(
      source: sourceList,
      compute: (list) => list.where((item) => item > 5).toList(),
    );
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: () {
            setState(() {
              sourceList.add(sourceList.length + 1);
            });
          },
          child: Text('Add Item'),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: filteredList.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('Item ${filteredList[index]}'),
              );
            },
          ),
        ),
      ],
    );
  }
}
回到顶部