felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.53k stars 3.37k forks source link

feat: Enhance BlocBuilder with Dependencies for Selective State Changes #4195

Closed mibaloch closed 2 weeks ago

mibaloch commented 2 weeks ago

Description

Suppose my Bloc state has 10 state variables. I have a component that should be rebuilt whenever either one of foo, bar, or foobar variables change in the state. Currently, the solution would be to use the buildWhen method, like so:

BlocBuilder<BlocA, BlocAState>(
  buildWhen: (previous, current) {
    return previous.foo != current.foo || previous.bar != current.bar || previous.foobar != current.foobar;
  },
  builder: (context, state) {
    // return widget
  },
);

But as you might see, it's verbose as we have to explicitly check for the equality of each variable between the previous and current state.

Desired Solution

A better and cleaner way to approach this could be like this:

BlocBuilder<BlocA, BlocAState>(
  buildDependencies: (state) => [state.foo, state.bar, state.foobar],
  builder: (context, state) {
    // return widget
  },
);

In this approach, we simply pass an array of the state variables whose changes we want to listen to and rebuild the component. The equality shall be checked implicitly between the previous and current state under the hood.

Advanced Usage

The approach described in the desired solution further opens the door to two separate cases. Here are the cases along with the suggested solutions.

  1. The component should rebuild when any of the dependencies change.

      BlocBuilder<BlocA, BlocAState>(
            buildOnAny: (state) => [state.foo, state.bar, state.foobar],
            builder: (context, state) {
                  // return widget
            },
        );
  2. The component should rebuild when all of the dependencies change.

      BlocBuilder<BlocA, BlocAState>(
            buildOnAll: (state) => [state.foo, state.bar, state.foobar],
            builder: (context, state) {
                  // return widget
            },
        );

P.S. The solution is also applicable for BlocListener and BlocConsumer widgets, with the additional listenOnAll and listenOnAny parameters.

felangel commented 2 weeks ago

Hi @mibaloch 👋 Thanks for opening an issue!

buildWhen and listenWhen should only be used for micro-optimizations (I'm even considering removing them in the future since they are misused often and usually BlocSelector or context.select is what you want). buildWhen does not guarantee that builds will not happen so it sounds like what you're looking for instead is context.select or BlocSelector.

I'd recommend that you refactor your code from using BlocBuilder + buildWhen to just a BlocSelector like:

BlocSelector<BlocA, BlocAState>(
  selector: (state) => (state.foo, state.bar, state.foobar),
  builder: (context, state) {
    // return widget
  },
);

You can also use context.select like:

@override
Widget build(BuildContext context) {
  final results = context.select((BlocA bloc) => (bloc.state.foo, bloc.state.bar, bloc.state.foobar));
  // return widget.
}

You can refer to the documentation for more info.

Hope that helps! Closing for now but happy to continue the conversation if you have any follow up questions 👍