Skip to content

集合类型

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'),
            ),
          ],
        ),
      ],
    );
  }
}