集合类型
Oref 提供响应式的 List、Map 与 Set 封装。
ReactiveList
dart
final items = ReactiveList<int>.scoped(context, [1, 2, 3]);
items.add(4);排序
dart
batch(() {
items.sort((a, b) => a.compareTo(b));
});去重
dart
batch(() {
final unique = items.toSet().toList();
items
..clear()
..addAll(unique);
});批量更新
dart
batch(() {
for (var i = 0; i < items.length; i++) {
items[i] = items[i] * 2;
}
});ReactiveMap
dart
final map = ReactiveMap<String, int>.scoped(context, {'a': 1});
map['b'] = 2;批量更新
dart
batch(() {
map['a'] = 10;
map['b'] = 20;
});批量删除
dart
batch(() {
for (final key in ['a', 'b']) {
map.remove(key);
}
});ReactiveSet
dart
final tags = ReactiveSet<String>.scoped(context, {'flutter'});
tags.add('signals');去重
dart
batch(() {
tags.addAll(['signals', 'fast', 'signals']);
});批量增删
dart
batch(() {
tags.addAll(['async', 'widget']);
tags.removeAll(['fast']);
});读取会 track(),写入会 trigger(),因此访问过的 UI 会在变化时自动更新。
Flutter 示例(来自 example 应用)
dart
class WalkthroughSection extends StatelessWidget {
const WalkthroughSection({super.key});
@override
Widget build(BuildContext context) {
final query = signal<String>(context, '');
final items = ReactiveList.scoped(context, [
'Aurora',
'Comet',
'Nebula',
'Orion',
'Pulsar',
]);
final nextId = signal<int>(context, 1);
final controller = useMemoized(context, () => TextEditingController());
effect(context, () {
onEffectDispose(controller.dispose);
});
final filtered = computed<List<String>>(context, (_) {
final q = query().trim().toLowerCase();
if (q.isEmpty) return List.unmodifiable(items);
return items.where((item) => item.toLowerCase().contains(q)).toList();
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
key: const Key('walkthrough-query'),
controller: controller,
decoration: InputDecoration(
labelText: 'Search',
suffixIcon: IconButton(
tooltip: 'Clear filter',
onPressed: () {
controller.clear();
query.set('');
},
icon: const Icon(Icons.clear),
),
),
onChanged: query.set,
),
const SizedBox(height: 8),
SignalBuilder(
builder: (context) {
final results = filtered();
final total = items.length;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () {
items.add('Nova ${nextId()}');
nextId.set(nextId() + 1);
},
child: const Text('Add item'),
),
OutlinedButton(
onPressed: total == 0 ? null : () => items.removeLast(),
child: const Text('Remove last'),
),
TextButton(
onPressed: () {
controller.clear();
query.set('');
},
child: const Text('Clear filter'),
),
],
),
const SizedBox(height: 8),
Text('showing ${results.length} of $total'),
const SizedBox(height: 4),
for (final item in results) Text(item),
],
);
},
),
],
);
}
}dart
class ReactiveMapSection extends StatelessWidget {
const ReactiveMapSection({super.key});
@override
Widget build(BuildContext context) {
final inventory = ReactiveMap<String, int>.scoped(context, {
'apples': 2,
'oranges': 3,
});
final nextId = signal<int>(context, 1);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SignalBuilder(
builder: (context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final entry in inventory.entries)
Text('${entry.key}: ${entry.value}'),
],
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () {
inventory['apples'] = (inventory['apples'] ?? 0) + 1;
},
child: const Text('Bump apples'),
),
OutlinedButton(
onPressed: () {
final id = nextId();
inventory['item $id'] = id;
nextId.set(id + 1);
},
child: const Text('Add item'),
),
TextButton(
onPressed: () {
if (inventory.isNotEmpty) {
inventory.remove(inventory.keys.first);
}
},
child: const Text('Remove first'),
),
],
),
],
);
}
}dart
class ReactiveSetSection extends StatelessWidget {
const ReactiveSetSection({super.key});
@override
Widget build(BuildContext context) {
final tags = ReactiveSet<String>.scoped(context, {'flutter', 'signals'});
final nextId = signal<int>(context, 1);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SignalBuilder(
builder: (context) => Wrap(
spacing: 8,
children: [for (final tag in tags) Chip(label: Text(tag))],
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: () {
final id = nextId();
tags.add('tag-$id');
nextId.set(id + 1);
},
child: const Text('Add tag'),
),
OutlinedButton(
onPressed: tags.isEmpty ? null : () => tags.remove(tags.first),
child: const Text('Remove one'),
),
TextButton(
onPressed: () => tags.clear(),
child: const Text('Clear'),
),
],
),
],
);
}
}