You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Riverpod 2.0中.select()无法阻止不必要重建的问题

StateNotifierProvider.select()使用误区及问题解析

误区1:认为select能阻止父Widget重建

你的核心问题在于把两个ref.watch都放在了TestWidget的build方法里。ConsumerWidget的特性是:只要它监听的任何一个provider发生变化(且监听条件满足),整个build方法就会重新执行。

也就是说,当你点击Inc 1触发count1更新时,第一个ref.watch的select条件满足(值变化),会触发TestWidget的build重新运行。此时第二个ref.watch也会被执行一遍,导致你看到第二个Text的print语句被调用——哪怕它的select返回值没有变化。

你预期的“仅第一个Text重建”需要让每个Text的监听独立于父Widget,方法是把每个Text包裹在单独的Consumer中,或者拆成独立的ConsumerWidget,这样只有当自身监听的select值变化时,才会触发局部重建,不会影响其他部分。

误区2:未正确处理状态类的相等性判断

你的Counter类没有重写==运算符和hashCode方法,Riverpod默认通过引用相等来判断状态是否变化。每次调用inc1或inc2时,你都创建了一个新的Counter实例,哪怕其中一个字段的值没有变化,Riverpod也会认为状态发生了变化,进而通知所有监听该provider的地方。

虽然这不是第二个Text触发print的直接原因,但会导致不必要的状态通知,也是你看到重复打印的潜在因素之一。

解决方法

1. 重构Widget结构,实现局部监听

把每个Text用Consumer包裹,让它们的监听独立:

class TestWidget extends StatelessWidget {
  const TestWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Consumer(
            builder: (context, ref, child) {
              final count1 = ref.watch(counterProvider.select((value) => value.count1));
              print("rebuilt counter 1 Text with val: $count1");
              return Text(count1.toString());
            },
          ),
          Consumer(
            builder: (context, ref, child) {
              final count2 = ref.watch(counterProvider.select((value) => value.count2));
              print("rebuilt Counter 2 Text with val: $count2");
              return Text(count2.toString());
            },
          ),
          ElevatedButton(
            onPressed: () => ref.read(counterProvider.notifier).inc1(),
            child: const Text("Inc 1"),
          ),
          ElevatedButton(
            onPressed: () => ref.read(counterProvider.notifier).inc2(),
            child: const Text("Inc 2"),
          ),
        ],
      ),
    );
  }
}

2. 让状态类支持值相等判断

修改Counter类,重写==和hashCode:

class Counter {
  final int count1;
  final int count2;

  Counter(this.count1, this.count2);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Counter &&
          runtimeType == other.runtimeType &&
          count1 == other.count1 &&
          count2 == other.count2;

  @override
  int get hashCode => count1.hashCode ^ count2.hashCode;
}

这样修改后:

  • 点击Inc 1时,只有第一个Consumer会重建并打印日志,第二个Consumer不会触发任何操作;
  • 状态更新时,Riverpod会先判断新旧Counter的值是否相等,避免不必要的通知。

关于重复重建的说明

你看到的第一个Text重复打印两次,是因为原代码中TestWidget的build被触发了两次(可能是Flutter框架的正常重建逻辑,比如布局变化)。当你用Consumer包裹后,局部重建只会触发一次,重复打印的问题也会解决。

内容的提问来源于stack exchange,提问作者MohaAmiry

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 21:30:55