felangel / bloc

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

BlocListener and BlocBuilder questions #278

Closed francobellu closed 5 years ago

francobellu commented 5 years ago

@felangel , Thanks for your great work. I'm using a slightly different version of your LoginForm: In my case it shows the login failure on a dialog The other difference is that all the authentication result states are generated only when the login button is pressed. If the credentials are wrong a dialog is shown.

I was initially using a StreamBuilder (as suggested by Didier Boelens )to handle the login states stream with an initialData = AuthState.NOT_LOGGED_IN but I saw that during rebuilds of the form the failure alert was always showing up instead of only once. So I started debugging and I noticed that after the first failed attempt of login and a correct handling of the AuthState.FAILED with a dialog, during subsequent rebuilds ( triggered simply by form validations, not by auth state changes) the stream builder producing again the dialog and it had a connectionState.waiting AND the snapshot.data was AuthState.FAILED which which is a value coming from the stream which to me is in contradiction with the fact that the connection is not set up yet. ( https://stackoverflow.com/questions/55919816/streambuilder-snapshot-is-always-connectionstate-waiting , still unanswered )

Then I come across your package and noticed that the StreamListener addressed that issue and I reimplemented the form using the BlocListener and a BlocBuilder and the thing finally works. Therefore my questions are:

1) Isn't the BlocListener + BlocBuilder pattern a general solution that can successful apply to any kind of stream ( not limited to blocs) where the values needs to be handled with an action other than with a builder function to update the ui?

2) In what does a BlocBuilder differ from a StreamBuilder?

3) Why is the BlocBuilder skipping the first state on line 92? _subscription = widget.bloc.state.skip(1).listen((S state) {...}

Thanks for your time

franco

felangel commented 5 years ago

Hi @francobellu πŸ‘‹ Thanks for opening an issue and great questions!

  1. Yes, you can theoretically apply the same pattern more generally; however, it was designed and intended to be used specifically within this library πŸ‘
  2. BlocBuilder optimizes for fewer rebuilds as compared to StreamBuilder because it has a much more narrow focus and also has a simplified api which abstracts the concept of AsyncSnapshots so that as a developer you just get the data you want from the bloc state stream.
  3. This is an optimization to avoid an unnecessary rebuild when initially subscribing to the bloc state stream. Since the bloc.state is a BehaviorSubject under the hood, you will get the most recent value when you subscribe. This is not necessary in the bloc library because the only time this is valuable is for initial data which is already declared within the bloc's initialState.

Hope that helps and again great questions πŸ’―

francobellu commented 5 years ago

@felangel , Thanks for your quick reply.

Regarding 3 I still have a doubt: -So how would my BlocBuilder be able to process the initial state (maybe a showing an initial message) if that state is skipped? -Also why do you use a BehaviorSubject if your intention is to skip the the initial value?

felangel commented 5 years ago

@francobellu no problem!

The initialState is defined in your bloc implementation so and can be accessed via myBloc.initialState.

BehaviorSubject is still useful because it allows you to have access to the latest value emitted by the subject making it really easy to support currentState. In addition, internal implementation details benefit from this as well as BlocListener.

Hope that answers your questions πŸ‘

francobellu commented 5 years ago

@felangel , Sorry I don't quite understand how could I use the myBloc.initialState data inside the BlocBuilder. Certainly the StreamBuilder has that ability and the correspondent parameter.

So, given my code, how would you build the widget when the state is AuthState.UNKNOWN ( which is the bloc initial state)?

class ErrorDialog extends StatelessWidget {
  //print("FB:_showErrorDialog: ${authBloc.state.toList()}");

  Widget build(BuildContext context) {
    Bloc<AuthenticationEvent, AuthenticationState> authBloc =
        BlocProvider.of<FirebaseAuthBloc>(context);
    print("FB:_AuthenticationFormState:build() ");
    return BlocListener<AuthenticationEvent, AuthenticationState>(
      // GET THE FIRST VALUES SYNCHRONOUSLY FROM THE LATEST VALUE OF THE STREAM
      bloc:
          authBloc, //AuthenticationState.notAuthenticated(), // AuthState.NOT_LOGGED_IN
      listener:
          (BuildContext streamBuilderContext, AuthenticationState authState) {
        print("FB: BlocListener:build() ");
        if (authState.state == AuthState.FAILED)
          showMyDialog(authState.failureReason.toString(), context);
      },
      child: BlocBuilder(
        bloc: authBloc,
        builder:
            (BuildContext streamBuilderContext, AuthenticationState authState) {
          print("FB: BlocBuilder:build() authState: $authState");
          switch (authState.state) {
            case AuthState.UNKNOWN:
              print("FB:_AuthenticationFormState: AuthState.UNKNOWN $authState.state");
              break;
            case AuthState.FAILED:
              print("FB:_AuthenticationFormState: AuthState.FAILED Reason: ${authState.failureReason}");
              break;
            case AuthState.NOT_LOGGED_IN:
            case AuthState.LOGGED_IN:
            case AuthState.WAITING_RESPONSE:
          }
          return Container(
            height: 0.0,
            width: 0.0,
          );
        },
      ),
    );
  }
}
felangel commented 5 years ago

@francobellu I think I might've been unclear. BlocBuilder takes a bloc as an argument and can access the initialState internally. You as a developer, don't have to worry about what's going on under the hood (unless you're curious). The example you've included should work fine even if AuthState.UNKNOWN is the initial state. Are you having trouble getting it to work properly? I thought you were just trying to understand how the underlying implementation works and I apologize for any confusion.

francobellu commented 5 years ago

@felangel, No problem, I'm currently unable to build the widget when the bloc is in its initial state, AuthState.UNKNOWN, because the BlocBuilder skips it. I'd like to be able to build the widget specifically for the initial state. How would you do it?

felangel commented 5 years ago

Can you please share a link to a sample app that demonstrates the problem you’re having?

francobellu commented 5 years ago

@felangel , Sorry for the delay, here's my project. https://www.dropbox.com/s/epinz3juxnekaj3/TheBoringShow.zip?dl=0

You'll see that when you launch it you'll get to the authentication form

what I don't understand is 1) Why is that the bloc's initialData, AuthenticationState.unknown(), is actually not even ever emitted by the bloc? So what's the point of having setting initialData? The BehaviorSubject documentation says:"

... It is possible to provide a seed value that will be emitted if no items have been added to the subject.

In my case until the first event (AuthenticationEventCheckAuthStatus) is dispatched, no other item has been added to the subject, so I still don't understand why the AuthenticationState.unknown() state isn't yielded.

2) As you can see there are 3 widget listening to the authBloc: the landingPage is using a StreamBuilder, then the BlocListener and the BlocBuilder inside the ErrorDialog. If you look at the logs, they behave exactly the same in terms of states received. I don't understand why because the BlocBuilder, compared with the BlocListener is skipping the first element. I also don't see the difference between the BlocBuilder and the StreamBuilder in terms of avoiding due to unnecessary rebuilds.

3) Talking about unnecessary rebuilds I do see a lot of them in the Landing Page when I just move focus from one field to the other. I don't understand that, who does trigger them?

4) Finally, your line _stateSubject = BehaviorSubject<State>.seeded(initialState); is not compiling on my project, do you know why? Screenshot 2019-05-10 at 19 12 49

felangel commented 5 years ago

@francobellu no worries and thanks for providing the application code.

  1. The initialState is required because when you use BlocBuilder and provide your bloc that initialState is what is provided to the builder before any events have been dispatched.

  2. The BlocBuilder implementation automatically sets the state to initialState before subscribing to the bloc stream which is why the skip(1) is used. With BlocListener, we always want listener to be called whenever there is a new state (even on initialState) which is why there is no skip(1). The BlocBuilder builder method is called on every widget build (triggered by the flutter framework) whereas the BlocListener listener is triggered only by changes in the bloc's state stream.

  3. Check this issue out for more information

  4. The bloc library has a dependency on rxdart ^0.22.0 so you need to update your local rxdart version. The error you're describing was a breaking change introduced by rxdart from 0.20.0 to 0.21.0.

Hope that helps πŸ‘

francobellu commented 5 years ago

@felangel, Regarding point 1 I try to rephrase my problem: In my case the builder function of the BlocBuilder is not able to receive the AuthenticationState.unknown() state and to build the blocBuilder accordingly. So if I wants to show a Text widget that shows the content of _errorMessage when AuthState.UNKNOWN, I can't.

BlocBuilder(
        bloc: authBloc,
        builder:
            (BuildContext streamBuilderContext, AuthenticationState authState) {
          print("FB:_AuthenticationFormState BlocBuilder: authState: $authState");
          switch (authState.state) {
            case AuthState.UNKNOWN: 
           _errorMessage = " Auth State Unknown"; // This is never executed
            return;
            case AuthState.FAILED:
            _errorMessage = " authenticationFailed";
            return;
            case AuthState.NOT_LOGGED_IN:
            case AuthState.LOGGED_IN:
            case AuthState.WAITING_RESPONSE:
          }
          return Container(
            height: 0.0,
            width: 0.0,
          );
felangel commented 5 years ago

@francobellu I took another look and it looks like in landing_page.dart you have

@override
  void initState() {
    //    print("FB: _LandingPageState: initState()");
    final authBloc = BlocProvider.of<FirebaseAuthBloc>(context);
    authBloc.dispatch(AuthenticationEventCheckAuthStatus());
    super.initState();
  }

This is updating the AuthenticationBloc state from AuthState.UNKNOWN to AuthState.WAITING_RESPONSE.

You can look at the transitions in the console to see the events and how they are changing the state of your blocs.

flutter: Transition { currentState: AuthState.UNKNOWN, event: Instance of 'AuthenticationEventCheckAuthStatus', nextState: AuthState.WAITING_RESPONSE }
flutter: Transition { currentState: AuthState.WAITING_RESPONSE, event: Instance of 'AuthenticationEventCheckAuthStatus', nextState: AuthState.NOT_LOGGED_IN }

Hope that helps πŸ‘

francobellu commented 5 years ago

@felangel Yes, that's correct. So, if I'm understanding correctly, BlocBuilder widgets don't need to be rebuilt for the initialState, because the first transition happens immediately so the BlocBuilder will only need building starting from the second state of the stream ( hence the skip(1)).

So a typical sequence of state for my app would be:

flutter: Transition { currentState: AuthState.UNKNOWN, event: Instance of 'AuthenticationEventCheckAuthStatus', nextState: AuthState.WAITING_RESPONSE }
flutter: Transition { currentState: AuthState.WAITING_RESPONSE, event: Instance of 'AuthenticationEventCheckAuthStatus', nextState: AuthState.NOT_LOGGED_IN }
flutter: Transition { currentState: AuthState.NOT_LOGGED_IN, event: Instance of 'AuthenticationEventLogin', nextState: AuthState.WAITING_RESPONSE }

Which in turn would create a stream of

AuthState.UNKNOWN ( this being the seed)
AuthState.WAITING_RESPONSE
AuthState.NOT_LOGGED_IN

So the BlocBuilder skips the AuthState.UNKNOWN because is not interested in rebuilding for this ephemeral state and I can see that from my logs:

flutter: FB:_AuthenticationFormState BlocBuilder: authState: AuthState.WAITING_RESPONSE
flutter: FB:_AuthenticationFormState BlocBuilder: authState: AuthState.NOT_LOGGED_I

The BlocListener, on the other hand, doesn't skip any value so I wold expect it to receive all the 3 states but the log shows only the first 2:

FB:_AuthenticationFormState BlocListener: authState: AuthState.WAITING_RESPONSE
FB:_AuthenticationFormState BlocListener: authState: AuthState.NOT_LOGGED_IN

Why is that?

felangel commented 5 years ago

@francobellu the BlocBuilder does not skip the AuthState.UNKNOWN. If the skip(1) wasn't present, then the AuthState.UNKNOWN would be returned twice. Your logs are missing the UNKNOWN state because by the time BlocBuilder is instantiated, you've already changed the state of the AuthenticationBloc. Same thing applies to the BlocListener; it only listens once it's instantiated so if you instantiate it later (which is the case for your app), the listener will be triggered once with the latest state and then only for subsequent state changes. It will not play back all previous states.

francobellu commented 5 years ago

@felangel I think I've finally understood, please tell me if i'm correct:

So a typical sequence of events for my app would be:

flutter: Transition { currentState: AuthState.UNKNOWN, event: Instance of 'AuthenticationEventCheckAuthStatus', nextState: AuthState.WAITING_RESPONSE }
flutter: Transition { currentState: AuthState.WAITING_RESPONSE, event: Instance of 'AuthenticationEventCheckAuthStatus', nextState: AuthState.NOT_LOGGED_IN }
----> (BlocBuilder and BlocListener created now)<----
flutter: Transition { currentState: AuthState.NOT_LOGGED_IN, event: Instance of 'AuthenticationEventLogin', nextState: AuthState.WAITING_RESPONSE }

Which in turn would create a stream of

AuthState.UNKNOWN ( this being the seed)
AuthState.WAITING_RESPONSE
AuthState.NOT_LOGGED_IN

BlocListener: By the time it is created, it can just receive the new AuthState.NOT_LOGGED_IN state but he can also replay the last event AuthState.WAITING_RESPONSE ( being a behaviourSubject). So effectively he handles the following sequence of states:

AuthState.WAITING_RESPONSE
AuthState.NOT_LOGGED_IN

BlocBuilder ( and here is the most complex part): The BlocBuilder is a bit different because when is built it immediately handle the current state of the bloc (AuthState.WAITING_RESPONSE) due to its initState:

void initState() {
    super.initState();
    _state = widget.bloc.currentState;  // <----This state is then passed as param in the builder function
    _subscribe();
  }

So AuthState.WAITING_RESPONSE is the first state handled but then the BlocBuilder would also handle each of the states of the stream since its creation, skipping the first that is :

(AuthState.WAITING_RESPONSE) skipped
AuthState.NOT_LOGGED_IN

At the end the BlocBuilder ends up handling the same states as the BlocListener.

If all this makes sense why ( and this might be the last question) your prefer to handle AuthState.WAITING_RESPONSE setting state = widget.bloc.currentState in the initState rather than having the same state as part of the stream that is being listened to?

felangel commented 5 years ago

@francobellu I think you've mostly got it now πŸ‘

We need to set the state to currentState in initState because build can be called by flutter at any moment in time (before we receive the first state from the subscription) and we need to make sure that there is always a valid state. You can try cloning the code and making your modifications and you'll see that BlocBuilder's builder will initially be called with a null state.

francobellu commented 5 years ago

@felangel πŸ‘Yes, I finally got it. Thanks so much for your time and patience on this issue. I hope this thread can be useful for others to better understand this library. I am fairly new with streams and Rx stuff so sometimes it costs me a lot of effort to understand how they work. Thanks again!

Chimba123 commented 4 years ago

Hello, guys, I am having this problem with the bloc package

https://stackoverflow.com/questions/62328609/how-to-correctly-use-bloclistener-and-blocprovider-in-flutter-app

I am getting the errors:

  1. β€˜A value type of BlocListener<LoginBloc, LoginState> can’t be returned from the method build because it has a return type of widget’

    1. β€˜A value type of BlocProvider<Bloc<dynamic, dynamic>> can’t be returned from the method build because it has a return type of widget’
image image
narcodico commented 4 years ago

Hi @Chimba123 πŸ‘‹

Can you share a small repo/gist with the reproduction of your issues ? Thanks πŸ‘

Chimba123 commented 4 years ago

@RollyPeres I have just re-opened the project once again and the errors are gone?????? πŸ€·πŸΎβ€β™‚οΈ am confused

narcodico commented 4 years ago

Probably your analyzer was confused as well πŸ™‚. Glad it's working now ✌

davidAg9 commented 4 years ago

hello I am trying to register a user to firebase and I implemented bloc listener but it doesn't seem to be responding thus it does navigate or show error message as I have specified

Screenshot 2020-08-19 at 9 08 46 AM
jaykadamss commented 4 years ago

hiii sir can i use streambuilder and bloc builder both in the class for multiple datas

jaykadamss commented 4 years ago

???

narcodico commented 4 years ago

Hi @jaykadamss πŸ‘‹

Great enthusiasm πŸ‘ You can easily use multiple BlocBuilders to build different parts of your screen based on state slices; you could even use buildWhen to further optimize when your widgets rebuild.

Alternatively you can also use both StreamBuilder and BlocBuilder within the same widget to build parts of it, but with a proper layering of your app you shouldn't need to have to use a StreamBuilder.