Search code examples
flutterflutter-provider

Selector.shouldRebuild is not used and always rebuild widget


Versions

provider 6.0.5 (current latest)
Flutter 3.13.7
Dart 3.1.3

Code example

https://dartpad.dev/?id=50e340da1ea3c75f0aa66a32394db32d

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

Output

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

Problem

build is always called.
shouldRebuild is never called.

Question

What am I doing wrong?

P.S.

If change
final shouldInvalidateCache = oldWidget != widget ||
to
final shouldInvalidateCache = oldWidget == null ||
in provider/lib/src/selector.dart
then all work good:

Increment index: 1
Selector  index: 0, selector: 0
Should    index: 0, prev: 0, next: 0 
Selector  index: 1, selector: 1
Should    index: 1, prev: 0, next: 1 , NEED REBUILD
Build     index: 1
Selector  index: 2, selector: 0
Should    index: 2, prev: 0, next: 0 

Solution

  • The problem is the .watch methods rebuilds the tree, which rebuilds Selector each time, making it never get to shouldRebuild.

    Solution: Change the watch method to read, cause it's only needed once to build the related widgets.

    // change
    var values = context.watch<MyState>().values;
    // to
    var values = context.read<MyState>().values;
    

    Instead of using .watch results to update the sum, use a consumer.

    // change
    Text(
      "Summ: ${values.reduce((value, element) => value + element)}",
      textScaleFactor: 2,
    ),
    //to
    Consumer<MyState>(
      builder: (context, state, child) {
        return Text(
          "Summ: ${state.values.reduce((value, element) => value + element)}",
          textScaleFactor: 2,
        );
      },
    )
    

    Bonus You may access the MyState value from builder in selector.

    // this
    builder: (_, __, ___) {
      return Expanded(
        child: Center(
          child: Text(
            "${values[i]}",
            textScaleFactor: 3,
          ),
        ),
      );
    },
    // to
    builder: (_, value, ___) {
      return Expanded(
        child: Center(
          child: Text(
            "$value",
            textScaleFactor: 3,
          ),
        ),
      );
    },