skip to Main Content

i’m using state notifier and state notifier provider with select to only apply rebuild to specific field in the object. but the whole widget rebuild whether i selected or not.

i have the following example code to simplify my problem:

final counterProvider =
    StateNotifierProvider<CounterState, Counter>((ref) => CounterState());

class Counter {
  int count1;
  int count2;

  Counter(this.count1, this.count2);
}

class CounterState extends StateNotifier<Counter> {
  CounterState() : super(Counter(0, 0));

  void inc1() => state = Counter(state.count1 + 1, state.count2);

  void inc2() => state = Counter(state.count1, state.count2 + 1);
}

and the following consumer widget:

class TestWidget extends ConsumerWidget {

  const TestWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) => Scaffold(
      body: Column(
        children: [
      Text(ref.watch(counterProvider.select((value) {
        print("rebuilt counter 1 Text with val: ${value.count1}");
        return value.count1.toString();
      }))),
      Text(ref.watch(counterProvider.select((value) {
        print("rebuilt Counter 2 Text with val: ${value.count2}");
        return value.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"))
        ],
      ));
}

i’m excpecting when pressing the inc1() button to not rebuild the second text. only the first.

but the output in the console when i press inc1 for 3 times is the following:

I/flutter (19394): rebuilt counter 1 Text with val: 0
I/flutter (19394): rebuilt Counter 2 Text with val: 0
D/EGL_emulation(19394): app_time_stats: avg=417.64ms min=8.70ms max=4924.00ms count=13
I/flutter (19394): rebuilt counter 1 Text with val: 1
I/flutter (19394): rebuilt Counter 2 Text with val: 0
I/flutter (19394): rebuilt counter 1 Text with val: 1
I/flutter (19394): rebuilt Counter 2 Text with val: 0
D/EGL_emulation(19394): app_time_stats: avg=78.42ms min=2.63ms max=1171.43ms count=18
I/flutter (19394): rebuilt counter 1 Text with val: 2
I/flutter (19394): rebuilt Counter 2 Text with val: 0
I/flutter (19394): rebuilt counter 1 Text with val: 2
I/flutter (19394): rebuilt Counter 2 Text with val: 0
D/EGL_emulation(19394): app_time_stats: avg=34.74ms min=2.47ms max=721.10ms count=25
I/flutter (19394): rebuilt counter 1 Text with val: 3
I/flutter (19394): rebuilt Counter 2 Text with val: 0
I/flutter (19394): rebuilt counter 1 Text with val: 3
I/flutter (19394): rebuilt Counter 2 Text with val: 0

and i’m expecting in the console:

I/flutter (19394): rebuilt counter 1 Text with val: 0
I/flutter (19394): rebuilt Counter 2 Text with val: 0
D/EGL_emulation(19394): app_time_stats: avg=417.64ms min=8.70ms max=4924.00ms count=13
I/flutter (19394): rebuilt counter 1 Text with val: 1
D/EGL_emulation(19394): app_time_stats: avg=78.42ms min=2.63ms max=1171.43ms count=18
I/flutter (19394): rebuilt counter 1 Text with val: 2
D/EGL_emulation(19394): app_time_stats: avg=34.74ms min=2.47ms max=721.10ms count=25
I/flutter (19394): rebuilt counter 1 Text with val: 3

so what am i not understanding correctly about select() function?
and why the first text is rebuilt twice although the change occur once?

3

Answers


  1. Chosen as BEST ANSWER

    Thank you @Ruble, @Rémi Rousselet i did this solution before and it worked as i said.. but a problem occurs when i have a situation like this:

    class Counter {
      int count1;
      int count2;
      Color count1Color;
      Counter(this.count1, this.count2,this.count1Color);
    }
    

    and i can change the color the same way i did with inc1() and inc2(), through a procedure..

    Consumer(builder: (context, ref, _) =>
                        Text(ref.watch(counterProvider.select((value) =>
                            value.count1.toString()))
                          , style: TextStyle(color: ref.watch(counterProvider.select((value) => value
                              .count1Color))),)), 
                    // a color for count1 editable through a button for example.
                    // if i change the state of the color the the text
                    // will be rebuild too! so back to the same problem!
                    
                    Consumer(builder: (context, ref, _) =>
                        Text(ref.watch(counterProvider.select((value) =>
                            value.count2.toString())))),
    

    so where is the problem? there is still some situations for example where i want to watch a decoration of an object through a composite state notifier like this example above. the solution of nested consumer widgets doesn't always work, because in the situation above the style property doesn't accept a value of type consumer because it is not a widget.

    what can i do in situations like this?

    i need change the text of the counter and it's color using state notifier provider, without any redundant rebuilts.


  2. The select function is unrelated to the problem described.

    select is for skipping some updates which you don’t care about. In your care about the update, but want to rebuild fewer widgets.

    To do that the solution is to either:

    • Extract your individual texts as a separate ConnsumerWidget
    • wrap your texts into a Consumer

    Like:

    Column(
      children: [
        Consumer(builder: (context, ref, _) {
          return Text(ref.watch(fooProvider));
        }),
        Consumer(builder: (context, ref, _) {
          return Text(ref.watch(barProvider));
        }),
      ],
    )
    

    This way, an update on fooProvider will not rebuild the text which listens to barProvider

    Login or Signup to reply.
  3. try like this:

    class TestWidget extends ConsumerWidget {
    
      const TestWidget({
        Key? key,
      }) : super(key: key);
    
      @override
      Widget build(BuildContext context, WidgetRef ref) => Scaffold(
          body: Column(
            children: [
         Consumer(builder: (context, ref, _) { // added this
          Text(ref.watch(counterProvider.select((value) {
            print("rebuilt counter 1 Text with val: ${value.count1}");
            return value.count1.toString();
          }))),
         }),
         Consumer(builder: (context, ref, _) { // added this
          Text(ref.watch(counterProvider.select((value) {
            print("rebuilt Counter 2 Text with val: ${value.count2}");
            return value.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"))
            ],
          ));
    }
    

    This is because you are using ref, which is in the scope of TestWidget. You must use a separate widget or use the Consumer wrapper. This was described by @Rémi Rousselet

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search