Skip to content

上手示例

可搜索列表

这个示例结合了查询信号、响应式列表和 computed 过滤。

dart
final query = signal(context, '');
final items = ReactiveList.scoped(context, [
  'Aurora',
  'Comet',
  'Nebula',
  'Orion',
  'Pulsar',
]);

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();
});

UI 接线示例:

dart
TextField(
  decoration: const InputDecoration(labelText: 'Search'),
  onChanged: query.set,
);

SignalBuilder(
  builder: (context) {
    final results = filtered();
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('显示 ${results.length} 项'),
        for (final item in results) Text(item),
      ],
    );
  },
);

可以在示例应用中直接体验。

结算总价 + 自动保存

这个流程把 .set(...)computedeffect 组合在一起。

dart
final qty = signal(context, 1);
final unitPrice = signal(context, 19.0);
final discount = signal(context, 0.0); // 0%..30%

final subtotal = computed<double>(context, (_) => qty() * unitPrice());
final total = computed<double>(context, (_) => subtotal() * (1 - discount()));

final saves = signal(context, 0);
effect(context, () {
  total(); // 追踪 computed
  final current = untrack(() => saves());
  saves.set(current + 1); // 自动保存次数
});

.set(...) 更新输入:

dart
qty.set(qty() + 1);
discount.set(0.1); // 10% 活动
unitPrice.set(24.0);

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 CheckoutWorkflowSection extends StatelessWidget {
  const CheckoutWorkflowSection({super.key});

  @override
  Widget build(BuildContext context) {
    final qty = signal<int>(context, 1);
    final unitPrice = signal<double>(context, 19.0);
    final discount = signal<double>(context, 0.0);
    final saves = signal<int>(context, 0);

    final subtotal = computed<double>(context, (_) => qty() * unitPrice());
    final total = computed<double>(
      context,
      (_) => subtotal() * (1 - discount()),
    );

    effect(context, () {
      total();
      final current = untrack(() => saves());
      saves.set(current + 1);
    });

    String money(double value) => value.toStringAsFixed(2);
    String percent(double value) => '${(value * 100).round()}%';

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('qty: ${qty()}'),
        Text('unit: \$${money(unitPrice())}'),
        Text('subtotal: \$${money(subtotal())}'),
        Text('discount: ${percent(discount())}'),
        Text('total: \$${money(total())}'),
        Text('autosave runs: ${saves()}'),
        const SizedBox(height: 8),
        Wrap(
          spacing: 8,
          children: [
            ElevatedButton(
              onPressed: () => qty.set(qty() + 1),
              child: const Text('Add 1 item'),
            ),
            OutlinedButton(
              onPressed: qty() <= 1 ? null : () => qty.set(qty() - 1),
              child: const Text('Remove 1 item'),
            ),
            TextButton(
              onPressed: () => discount.set(discount() == 0 ? 0.1 : 0.0),
              child: const Text('Toggle 10% promo'),
            ),
            TextButton(
              onPressed: () => unitPrice.set(24.0),
              child: const Text('Set price = 24'),
            ),
          ],
        ),
      ],
    );
  }
}