Closed francobellu closed 5 years ago
Hi @francobellu π Thanks for opening an issue and great questions!
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.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 π―
@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?
@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 π
@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,
);
},
),
);
}
}
@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.
@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?
Can you please share a link to a sample app that demonstrates the problem youβre having?
@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?
@francobellu no worries and thanks for providing the application code.
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.
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.
Check this issue out for more information
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 π
@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,
);
@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 π
@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?
@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.
@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?
@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.
@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!
Hello, guys, I am having this problem with the bloc package
I am getting the errors:
βA value type of BlocListener<LoginBloc, LoginState> canβt be returned from the method build because it has a return type of widgetβ
Hi @Chimba123 π
Can you share a small repo/gist with the reproduction of your issues ? Thanks π
@RollyPeres I have just re-opened the project once again and the errors are gone?????? π€·πΎββοΈ am confused
Probably your analyzer was confused as well π. Glad it's working now β
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
hiii sir can i use streambuilder and bloc builder both in the class for multiple datas
???
Hi @jaykadamss π
Great enthusiasm π You can easily use multiple BlocBuilder
s 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
.
@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