felangel / bloc

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

[Feature Request] MultiBlocBuilder #538

Closed PhilipChng closed 3 years ago

PhilipChng commented 4 years ago

Sometimes I would need to access different blocs in the same widget, and the code goes pretty messy. I am thinking maybe we can have something like MultiBlocBuilder which serve the similar purpose of MultiRepositoryProvider and MultiBlocProvider.

Allan-Nava commented 4 years ago

@Allan-Nava as of now you can nest the BlocBuilders.

Ok, but the UI is: photo_2020-05-20_18-55-35

I have to fill the dropdown with categories

felangel commented 4 years ago

What is the issue? You can have a categories bloc which manages the state of the categories which is used by the dropdown section via a BlocBuilder.

Allan-Nava commented 4 years ago

What is the issue? You can have a categories bloc which manages the state of the categories which is used by the dropdown section via a BlocBuilder.

Right now the memory leak, and i need to show in the same time cause I do two http requests. Maybe this is the solution is:

you should probably create a single bloc for that feature and the bloc can handle making both network requests. If they can be made in parallel you can use Future.wait([...]). Then in your UI you just need one BlocBuilder.

Allan-Nava commented 4 years ago

What is the issue? You can have a categories bloc which manages the state of the categories which is used by the dropdown section via a BlocBuilder.

Right now the memory leak, and i need to show in the same time cause I do two http requests. Maybe this is the solution is:

you should probably create a single bloc for that feature and the bloc can handle making both network requests. If they can be made in parallel you can use Future.wait([...]). Then in your UI you just need one BlocBuilder.

I tried but I got the same problem and the app crash.

rafaelmpessoa commented 4 years ago

@felangel thanks for your support lib and package, which helps me a lot. I have one case, I have a widget ContinueButton which has two properties isLoading and isValid the isLoading state is controlled by the current page bloc, in this case, the PaidQrcodeResumeBloc. The isValid state is controlled by reusable Widget ValueField that has your own Bloc ValueFieldBloc. How I can change these properties values without MultiBlocBuilder ?

Example code:

BlocBuilder<ValueFieldBloc, ValueFieldState>(
              builder: (context, state) {
                return ContinueButton(
                  onPressed: () => context.bloc<PaidQrcodeResumeBloc>().add(
                        PaidQrcodeResumeOnPaid(
                          widget.paidQrcode
                              .copyWith(value: moneyController.numberValue),
                        ),
                      ),
                  isValid: state is ValueFieldValid,
                  text: Strings.paid,
                  isLoading: ,
                );
              },
            )
narcodico commented 4 years ago

Hi @rafaelmpessoa ๐Ÿ‘‹

It sounds that both isLoading and isValid should be part of the same bloc's state, ValueFieldBloc in your case.

You can also add an event to ValueFieldBloc to flip isLoading to true when ContinueButton is pressed.

Or you could even have a BlocListener on PaidQrcodeResumeBloc that would react to state changes and would add the proper event on ValueFieldBloc which will result in flipping isLoading.

Hope that gives you a couple of ideas on how you could improve your implementation ๐Ÿ‘

angel1st commented 4 years ago

@felangel - I believe there is at least one good use case over there and as many people insisting for the feature mentioned, there are few reasons for the request:

  1. Bloc reusability - you can create a BLoC and then use it in several places. This btw is part of BLoC concept
  2. BLoC maintainability - it is much easier to maintain (upgrade and test) smaller bloc with specific logic.
  3. Code readability - avoiding pyramid of doom I believe is good programming practice in general.

I will also put here my use case since from my standing point it makes sense:

I have a calendar widget, which can be either filtered by specific appointment types (e.g. work, holiday and etc) or change its view (day, week, month and etc.). Hence, I created two separate BLoC(s) - one for event filter EventFilterBloc and one for calendar view CalendarViewBloc. I would consider each one as a feature rather than a resource. I have a few other places with calendars, where I need CalendarViewBloc, but since they have different appointment types (not events), I don't need EventFilterBloc part there, I rather need other appointment types. Hence I created additional BLoCs - each for these different calendars and I combine them with CalendarViewBloc.

One more question - since we have MultiBlocProvider I guess some of the arguments behind creating it can apply to MultiBlocBuilder case, can't it?

Anyway - I will understand if the implementation is complex, but my firm belief is that it is a needed feature.

davidAg9 commented 4 years ago

I am nesting different bloc builders in my widget ,multi blocbuilder sounds great but what if you want to put a particular widget somewhere on the screen by this ,you will still end up back at the core implementation of flutter bloc and put that particular builder somewhere.My point is on let the bloc builder finds a way to behave like the body it will be all difficult manipulating the widget positions, I don't know if I have made sense but that's my view.

kiesman99 commented 4 years ago

Hey there ๐Ÿ˜„ I want to give some Impressions on this topic too.

I am currently working on a workout timer application. In this application I have a TimerBloc which handles Timer ticks and reduces the remaining time left. After some time I thought it would be awesome if the app has a burn-in-protection, because i did not want the users phones to break, because itโ€˜s displaying a text for a couple of minutes. So iโ€˜ve build a BurnInBloc.

The TimerBloc internally runs a Timer.periodic which triggers a tick event each second. The BurnInBloc internally runs a Timer.periodic which triggers a protect Event each 60sec. If a touch event is emitted the BurnInBloc Timer will get resetted.

I thought it would be a nice idea to seperate these featues, because of the benefits @angel1st mentioned.

Now in the UI I have to combine TimerBloc and BurnInBloc with builders, which looks kind of messy, or Iโ€˜d have to add the features of BurnInBloc into TImerBloc. The second option is not the one Iโ€˜d prefer, because the new TimerBloc would get kind of messy I think. Having nested BlocBuilder on the other hand is also not very nice UI-wise.

BUT I think if flutter_bloc would provide something like BlocBuilder5 itโ€˜ll lead to very messy Blocs written by others (Not saying my Blocs are well written ๐Ÿ˜„). Giving the option will tell developers itโ€˜s fine to have BlocBuilder in BlocBuilder in BlocBuilder. In the current situation i have to think about how i structure my blocs and if i should rethink the current architecture of my blocs.

I think we need to have something to combine small blocs that benefit from decoupling, or a guide/way to combine small blocs, but should prevent giving developers the feeling nesting 100 BlocBuilder is natural.

(Also thanks for maintaining this package and having a discussion about this with us all)

kranfix commented 4 years ago

Hello, I was thinking about the problem of multiple widget composition of BlocBuilder and BlocListener ,then I created an experimental package with the same API than flutter_bloc: https://pub.dev/packages/flutter_hooks_bloc

This package reimplements those widgets based on HookWidget (https://pub.dev/packages/flutter_hooks).

A widget that rebuilds depending on many bloc would be written like this:

class MyMultiBlocBuilder extends HookWidget {
  MyMultiBlocBuilder

  @override
  Widget build(BuildContext context){
    // onEmitted is called every time that the state is emitter in a cubit/bloc
    final cubitA = useBloc<CubitA, int>(onEmitted: (context, previousState, state){
      // with true, the widget rebuild, otherwise, it behave like a BlocListener
      return buildWhenA?.call(previousState, state);
    });
    final blocC = useBloc<BlocB, String>(onEmitted: (context, previousState, state){
      // If you also want to have a BlocListener behavior, you can add some code here
      if(listenWhen?.call(previousState, state) ?? true){
        listener(context, state);
      }
      return buildWhenB?.call(previousState, state);
    });
    return Column(
      children: [
        Text('${cubitA.state}'),
        Text('${blocB.state}'),
      ],
    );
  }
}
kranfix commented 4 years ago

Hello, I was thinking about the problem of multiple widget composition of BlocBuilder and BlocListener ,then I created an experimental package with the same API than flutter_bloc: https://pub.dev/packages/flutter_hooks_bloc

This package reimplements those widgets based on HookWidget (https://pub.dev/packages/flutter_hooks).

A widget that rebuilds depending on many bloc would be written like this:

class MyMultiBlocBuilder extends HookWidget {
  const MyMultiBlocBuilder({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context){
    // onEmitted is called every time that the state is emitter in a cubit/bloc
    final cubitA = useBloc<CubitA, int>(onEmitted: (context, previousState, state){
      // with true, the widget rebuild, otherwise, it behave like a BlocListener
      return buildWhenA?.call(previousState, state) ?? true;
    });

    final blocB = useBloc<BlocB, String>(onEmitted: (context, previousState, state){
      // If you also want to have a BlocListener behavior, you can add some code here
      if(listenWhen?.call(previousState, state) ?? true){
        listener(context, state);
      }
      return buildWhenB?.call(previousState, state) ?? true;
    });

    // always rebuild when cubit emits a new state
    final cubitC = useBloc<CubitC, double>();

    return Column(
      children: [
        Text('${cubitA.state}'),
        Text('${blocB.state}'),
        Text('${cubitC.state}'),
      ],
    );
  }
}
kiesman99 commented 4 years ago

Hey @kranfix,

I think your solution looks good and would implement everything we need but there still some issues I am seeing here:

  1. Following the discussion here and especially this comment flutter_hooks currently is not supposed to be an alternative state management solution. It is not ready yet and just a POC. Therefore, I would not use this approach as an official guide to solve this kind of problem.
  2. While this solution looks slick and like something that is written very easily with n-number of needed BLoCs this might be also a Problem. As I stated in my previous comment (IMO) we do not want developers to think its good practice to use 5 BlocBuilder nested in each other. Giving a solution similar to yours we make it pretty easy to think nesting 5 BlocBuilder is looking normal.
  3. @felangel created an issue in which he wants to discuss if he wants to keep provider as a dependency. Therefore, I don't think adding flutter_hooks as a dependency would not be the best move. You would just depend on another package.

Maybe we should look more into Bloc-To-Bloc-Communication to solve this kind of problem.

This would give the option to have one or more separate Blocs which are combined through communication. Problem here is also combining multiple Blocs into one would result that including this one "SuperBloc" in the UI would seem pretty normal and not very problematic to the developer.

kiesman99 commented 4 years ago

@felangel Does it hurt performance somehow to have multiple BlocBuilder nested or is this something we can do without a problem? If not my 2. argument is pretty much invalid. (If so sorry ๐Ÿ˜„)

kranfix commented 4 years ago

@kiesman99 yeah. I know that flutter-hooks is POC, that's why I said flutter_hooks_bloc is an experimental package. But, it simplify many tasks that are not easy without hooks.

felangel commented 4 years ago

@kiesman99 nested BlocBuilders will not be less performant than hooks because at the end of the day if you want nested BlocBuilders it usually means you want something to rebuild if any of the bloc states change. In those cases, thereโ€™s nothing technically wrong with nested BlocBuilders although in my experience itโ€™s usually a sign that the blocs are data-driven and not feature-driven. Ideally, there should be one bloc per feature in which case there should be less of a need to nest BlocBuilders. Hope that helps ๐Ÿ‘

angel1st commented 4 years ago

although in my experience itโ€™s usually a sign that the blocs are data-driven and not feature-driven. Ideally, there should be one bloc per feature in which case there should be less of a need to nest BlocBuilders. Hope that helps ๐Ÿ‘

@felangel - I believe there are at least a couple of examples e.g. this one and this one, where we posted our real-life examples, to show that, in more complex apps, it is not unusual to have two or even three nested BlocBuilders and this is not a sign of bad architectural approach. On the other hand, if the implementation of so-called MultiBlocBuilder is doable even as syntactic sugar, once you do it, you will have even more happy followers ๐Ÿ‘ Besides - I would like also to thank you for the great library and all your efforts to maintain and develop it!

Classy-Bear commented 3 years ago

I'm now developing an app and faced the same problem but I conclude it was unnecessary for the following:

The definition of BlocBuilder according to the documentation is:

BlocBuilder is a Flutter widget which requires a Bloc and a builder function. BlocBuilder handles building the widget in response to new states. Source

So BlocBuilder should be use only when a widget will response to new states.

This is wrong ```dart BlocBuilder( builder: (context, state) { return Scaffold( // State is not being handle here so no need to wrap this in a BlocBuilder body: Text( state, // State is handled here ), ); } ) ```
This is correct ```dart Scaffold( body: BlocBuilder(builder: (context, state) { return Text( state, // State is handled here ); }), ); ```
What should I do if I have a `Widget` that has multiple `Widget`s whit its own state? ```dart Scaffold( body: Column( children: [ BlocBuilder(builder: (context, state) { return Text( state, ); }), BlocBuilder(builder: (context, state) { return Text( state, ); }), ], ), ); ```
What should I do if I have a bloc that handles the errorText of a Textfield and another bloc that handles the hintText? Use an `enum`. ```dart Scaffold( body: Column( children: [ BlocBuilder(builder: (context, state) { return TextField( decoration: InputDecoration( hintText: state == Text.hintText ? 'show the hint' : '', errorText: state == Text.errorText ? 'show the error' : '', ), ); }), ], ), ); ```

Solutions to avoid this issue to be open again in the future:

People should know how to handle correctly the Bloc Concepts, that's why I opened this issue a week ago. This isn't part of the core functionality so doesn't add any value, it solves a problem but is not the way to do it.

Note: The code provided is for demonstration purposes.

Deepjyoti120 commented 3 years ago

I simply use:-

BlocBuilder<UsernameupdateCubit, UsernameupdateState>( builder: (context, state) { //Note this return BlocBuilder<QuantityCubit, QuantityState>( builder: (context, stateQuantity) { //i changed variable name return ElevatedButton(

Thank you

felangel commented 3 years ago

I simply use:-

BlocBuilder<UsernameupdateCubit, UsernameupdateState>( builder: (context, state) { //Note this return BlocBuilder<QuantityCubit, QuantityState>( builder: (context, stateQuantity) { //i changed variable name return ElevatedButton(

Thank you

You can also use context.watch

@override
Widget build(BuildContext context) {
  final userState = context.watch<UsernameUpdateCubit>().state;
  final quantityState = context.watch<QuantityCubit>().state;

  return ElevatedButton(...);
}
krish-bhanushali commented 2 years ago

@felangel I know this is an old issue but I am using two cubits to manage theme and language respectively with one states each and I am passing both as individual blocbuilders over the material app. So is this a correct way to do it? or I can use one single cubit for both.

zombie6888 commented 2 years ago

What if instead of MultiBlocBuilder provide MultiBlocSelector in order to to combine data from different states and rebuild ui when obtained data slice is changed. If you combine whole state it will work like MultiBlocBuilder

felangel commented 2 years ago

@felangel I know this is an old issue but I am using two cubits to manage theme and language respectively with one states each and I am passing both as individual blocbuilders over the material app. So is this a correct way to do it? or I can use one single cubit for both.

I recommend one bloc/cubit per feature state.

felangel commented 2 years ago

What if instead of MultiBlocBuilder provide MultiBlocSelector in order to to combine data from different states and rebuild ui when obtained data slice is changed. If you combine whole state it will work like MultiBlocBuilder

Can you provide a concrete use case in which this would be necessary? I recommend a single bloc/cubit per feature so ideally your widgets should not need to aggregate data from multiple bloc/cubit states.

zombie6888 commented 2 years ago

It's not necessary, but convenient. For example if you have have newsfeed which is contains articles and bloc for it and some other feed like favorite or something else which is also have bloc and articles. And you have article detail page. User can go to details page from both feeds by passing article id to details route. So i want to select article from both feeds and update ui whenever article in feeds is changed. I can merge streams, use nested blocBuilders or reorganize my buisness logic (by adding third bloc, pass one bloc to another or something else) but multiselector will be more convinient for me. I think it's possible to find more use cases. Selectors is good feature and the way to combine selectors could be like selectors improvement

Tananga commented 2 years ago

@felangel I have this idea for the long time: What If we use Bloc per Screen + Global Blocs for features like Authentication or Theme? Is this a very bad idea or it have something in it?

bbura commented 2 years ago

@felangel usecase: In the main.dart we want to globally use localisation bloc, theme bloc. I just think that nesting 2 blocbuilders is exactly like that: nasty. Anyway, good job with the bloc and also mason, great tools mate.

narcodico commented 2 years ago

@tananga we recommend a bloc per feature, which sometimes mean a bloc per screen if the screen covers the whole feature.

@bbura localization is UI logic and should be done in the presentation layer, not in the bloc.

S-ecki commented 2 years ago

I also agree with @felangel that something like a MultiBlocBuilder would add unnecessary complexity to Bloc and should not be part of the package. However, I needed something similar for a HomeScreen, where information from multiple blocs is read and I wanted a global loading state.

Disclaimer: In the end, I used RxDart to combine the state stream instead of using the BlocBuilder3

Still, I will share my code for the BlocBuilder3 here, as it will save time for people who might want it in the future:

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

typedef BlocBuilderCondition<S> = bool Function(S previous, S current);
typedef BlocWidgetBuilder3<StateA, StateB, StateC> = Widget Function(
  BuildContext,
  StateA,
  StateB,
  StateC,
);

class BlocBuilder3<
    BlocA extends StateStreamable<BlocAState>,
    BlocAState,
    BlocB extends StateStreamable<BlocBState>,
    BlocBState,
    BlocC extends StateStreamable<BlocCState>,
    BlocCState> extends StatelessWidget {

  const BlocBuilder3({
    Key? key,
    required this.builder,
    this.blocA,
    this.blocB,
    this.blocC,
    this.buildWhenA,
    this.buildWhenB,
    this.buildWhenC,
  }) : super(key: key);

  final BlocWidgetBuilder3<BlocAState, BlocBState, BlocCState> builder;

  final BlocA? blocA;
  final BlocB? blocB;
  final BlocC? blocC;

  final BlocBuilderCondition<BlocAState>? buildWhenA;
  final BlocBuilderCondition<BlocBState>? buildWhenB;
  final BlocBuilderCondition<BlocCState>? buildWhenC;

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<BlocA, BlocAState>(
      bloc: blocA,
      buildWhen: buildWhenA,
      builder: (context, blocAState) {
        return BlocBuilder<BlocB, BlocBState>(
          bloc: blocB,
          buildWhen: buildWhenB,
          builder: (context, blocBState) {
            return BlocBuilder<BlocC, BlocCState>(
              bloc: blocC,
              buildWhen: buildWhenC,
              builder: (context, blocCState) {
                return builder(context, blocAState, blocBState, blocCState);
              },
            );
          },
        );
      },
    );
  }
}
Livinglist commented 1 year ago

@S-ecki how did you combine the states using RxDart? Just curious.

S-ecki commented 1 year ago

@S-ecki how did you combine the states using RxDart? Just curious.

I took the state streams and used combineLatest if I remember correctly. Note that with combineLatest only emits after each stream emitted an event by default.

amirbahrawy commented 1 year ago

I simply use:-

BlocBuilder<UsernameupdateCubit, UsernameupdateState>( builder: (context, state) { //Note this return BlocBuilder<QuantityCubit, QuantityState>( builder: (context, stateQuantity) { //i changed variable name return ElevatedButton(

Thank you

You can also use context.watch

@override
Widget build(BuildContext context) {
  final userState = context.watch<UsernameUpdateCubit>().state;
  final quantityState = context.watch<QuantityCubit>().state;

  return ElevatedButton(...);
}

Thats awesome i used the same approach also you can use select if you need to listen on only value like this final userState = context.select((UsernameUpdateCubit cubit) => cubit.state);

dittombek commented 1 year ago

Please take a look of this flutter bloc documents https://bloclibrary.dev/#/flutterbloccoreconcepts?id=usage-2

For safe implementation of multiple blocbuilder, use Builder and context.watch.

@override
Widget build(BuildContext context) {
  return Builder(
    builder: (context) {
      final stateA = context.watch<BlocA>().state;
      final stateB = context.watch<BlocB>().state;
      final stateC = context.watch<BlocC>().state;

      return const Text('Hello World');
    },
  );
}
SantiiRepair commented 10 months ago

MultiBlocBuilder would be interesting, BlocBuilder2... doesn't look good, I would like to know why MultiBlocBuilder is not a good idea

Fingertips18 commented 6 months ago

For clean architecture purposes, MultiBlocBuilder, MultiBlocListener or MultiBlocConsumer would be useful. I encountered a situation where I wanted to add two global blocs on top of a particular feature (which also has a bloc itself). Here is the idea behind what I was trying to implement:

BlocBuilder<Global1Bloc, Global1State>(
      buildWhen: (previous, current) {
        return previous.loading != current.loading;
      },
      builder: (context, global1State) {
        return BlocConsumer<Global2Bloc, Global2State>(
          listenWhen: (previous, current) {
            return !current.loading;
          },
          listener: (context, state) {
            if(state.isSuccess) {...};
          },
          buildWhen: (previous, current) {
            return previous.loading != current.loading;
          },
          builder: (context, global2State) {
            return BlocBuilder<FeatureBloc, FeatureState>(
              buildWhen: (previous, current) {
                return previous.hasInput!= current.hasInput;
              },
              builder: (context, featureState) {
                return CustomTextButton(
                  bgColor: Colors.black,
                  label: "Confirm",
                  loading: featureState.loading || global1State.loading,
                  onPressed: {...},
                );
              },
            );
          },
        );
      },
    );
  }

This is working but its kinda messy to look at. I will stick to this pattern for the meantime but might follow https://github.com/felangel/bloc/issues/538#issuecomment-1711049615

zombie6888 commented 5 months ago

For clean architecture purposes, MultiBlocBuilder, MultiBlocListener or MultiBlocConsumer would be useful. I encountered a situation where I wanted to add two global blocs on top of a particular feature (which also has a bloc itself). Here is the idea behind what I was trying to implement:

BlocBuilder<Global1Bloc, Global1State>(
      buildWhen: (previous, current) {
        return previous.loading != current.loading;
      },
      builder: (context, global1State) {
        return BlocConsumer<Global2Bloc, Global2State>(
          listenWhen: (previous, current) {
            return !current.loading;
          },
          listener: (context, state) {
            if(state.isSuccess) {...};
          },
          buildWhen: (previous, current) {
            return previous.loading != current.loading;
          },
          builder: (context, global2State) {
            return BlocBuilder<FeatureBloc, FeatureState>(
              buildWhen: (previous, current) {
                return previous.hasInput!= current.hasInput;
              },
              builder: (context, featureState) {
                return CustomTextButton(
                  bgColor: Colors.black,
                  label: "Confirm",
                  loading: featureState.loading || global1State.loading,
                  onPressed: {...},
                );
              },
            );
          },
        );
      },
    );
  }

This is working but its kinda messy to look at. I will stick to this pattern for the meantime but might follow #538 (comment)

Instead of buildwhen and listenWhen use selectors (context.select<...>(...)). You don't need blocBuilders here at all. You can rewrite your code with one listener and couple of selectors. You will be surprised how clean your code will become. It's not realted to clean architechture beacuse it's all about presentation layer