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

Selector.shouldRebuild未调用且Widget始终重建,技术问题求助

Provider中Selector的shouldRebuild未触发问题排查

版本信息

provider 6.0.5(当前最新版)
Flutter 3.13.7
Dart 3.1.3

代码示例

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: ChangeNotifierProvider<MyState>(
          create: (context) => MyState(),
          child: const Body(),
        ),
      ),
    );
  }
}

class MyState with ChangeNotifier {
  List<int> values = [0, 0, 0];
  void increment(int key) {
    values[key]++;
    notifyListeners();
  }
}

class Body extends StatelessWidget {
  const Body({super.key});

  @override
  Widget build(BuildContext context) {
    var values = context.watch<MyState>().values;
    return Column(
      children: [
        Row(
          children: [
            for (int i = 0; i < values.length; i++)
              Selector<MyState, int>(
                selector: (_, state) {
                  print("Selector  index: $i, selector: ${state.values[i]}");
                  return state.values[i];
                },
                shouldRebuild: (previous, next) {
                  print(
                      "Should    index: $i, prev: $previous, next: $next ${previous != next ? ", NEED REBUILD" : ""}");
                  return previous != next;
                },
                builder: (_, __, ___) {
                  print("Build     index: $i");
                  return Expanded(
                      child: Center(
                          child: Text(
                    "${values[i]}",
                    textScaleFactor: 3,
                  )));
                },
              )
          ],
        ),
        Row(
          children: [
            for (int i = 0; i < values.length; i++)
              Expanded(
                child: Center(
                  child: TextButton(
                    onPressed: () {
                      print("-" * 20);
                      print("Increment index: $i");
                      context.read<MyState>().increment(i);
                    },
                    child: Text("Increment $i"),
                  ),
                ),
              )
          ],
        ),
        Center(
          child: Text(
            "Summ: ${values.reduce((value, element) => value + element)}",
            textScaleFactor: 2,
          ),
        ),
      ],
    );
  }
}

输出日志

Selector  index: 0, selector: 0
Build     index: 0
Selector  index: 1, selector: 0
Build     index: 1
Selector  index: 2, selector: 0
Build     index: 2
--------------------
Increment index: 1
Selector  index: 0, selector: 0
Build     index: 0
Selector  index: 1, selector: 1
Build     index: 1
Selector  index: 2, selector: 0
Build     index: 2

问题现象

  • build方法每次状态变化都会被调用
  • shouldRebuild方法从未执行

问题原因

核心问题出在Body组件的build逻辑中:调用context.watch<MyState>()会让Body在MyState变化时完全重建。

当Body重建时,内部Row里通过for循环生成的Selector都是全新的组件实例。Provider的Selector内部会判断新旧组件实例是否相等(oldWidget != widget),如果不相等就直接失效缓存,跳过shouldRebuild检查,直接重新执行selector和builder方法,这就是shouldRebuild日志看不到、所有Selector都被强制build的原因。

修复方案

方案一:移除全局watch,避免Body整体重建

删掉Body中的context.watch<MyState>(),拆分组件或用Selector/Consumer按需获取数据,让Body不会因MyState变化而重建:

修改后的Body示例:

class Body extends StatelessWidget {
  const Body({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          children: [
            for (int i = 0; i < 3; i++)
              Selector<MyState, int>(
                selector: (_, state) {
                  print("Selector  index: $i, selector: ${state.values[i]}");
                  return state.values[i];
                },
                shouldRebuild: (previous, next) {
                  print(
                      "Should    index: $i, prev: $previous, next: $next ${previous != next ? ", NEED REBUILD" : ""}");
                  return previous != next;
                },
                builder: (_, value, ___) {
                  print("Build     index: $i");
                  return Expanded(
                      child: Center(
                          child: Text(
                    "$value",
                    textScaleFactor: 3,
                  )));
                },
              )
          ],
        ),
        Row(
          children: [
            for (int i = 0; i < 3; i++)
              Expanded(
                child: Center(
                  child: TextButton(
                    onPressed: () {
                      print("-" * 20);
                      print("Increment index: $i");
                      context.read<MyState>().increment(i);
                    },
                    child: Text("Increment $i"),
                  ),
                ),
              )
          ],
        ),
        Selector<MyState, int>(
          selector: (_, state) => state.values.reduce((a, b) => a + b),
          builder: (_, sum, ___) => Center(
            child: Text(
              "Summ: $sum",
              textScaleFactor: 2,
            ),
          ),
        ),
      ],
    );
  }
}

方案二:给Selector添加唯一Key

如果保留Body的watch逻辑,给每个Selector添加ValueKey(i),确保Body重建时,Provider能识别同一索引对应的Selector是同一个组件实例,这样oldWidget == widget成立,缓存不会失效,shouldRebuild会正常执行:

修改for循环内的Selector:

Selector<MyState, int>(
  key: ValueKey(i), // 添加唯一Key
  selector: (_, state) {
    print("Selector  index: $i, selector: ${state.values[i]}");
    return state.values[i];
  },
  // 其余代码不变
)

补充说明

修改provider源码中selector.dart里的shouldInvalidateCache判断逻辑,将oldWidget != widget ||改为oldWidget == null ||,会跳过组件实例变化的检查,强制使用缓存,此时shouldRebuild能正常执行,但这属于临时hack,不推荐作为正式解决方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 01:58:11