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
更多关于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]}'),
);
},
),
),
],
);
}
}