BlocListener not being executed after Cubit function call - flutter

So, I have a cubit with a function that POSTs some data. I have three states, DataLoading, DataError and DataLoaded for my cubit.
When the user taps on a button, I call the function in the cubit. After that, I have a BlocListener to wait until the Cubit emits the DataLoaded state. The issue is that the listener is reacting to the state changes.
Button(
text: 'Add',
onTap: () {
final data = _textController.text;
context.read<PostDataCubit>().post(data);
BlocListener<PostDataCubit, PostDataState>(
listener: (context, state) {
if (state is DataLoaded) {
// navigate to another route
} else if (state is DataError) {
// show error
}
},
);
}
),
I've tried using await on the read() call but that didn't work. How do I react to the state changes here? Thanks.

This BlocListener isn't listening because you have added the listener inside a function instead of adding it in widget tree. Wrap your button inside BlocConsumer widget and it will works fine. Have a look into below code.
BlocListener<PostDataCubit, PostDataState>(
listener: (context, state) {
if (state is DataLoaded) {
// navigate to another route
} else if (state is DataError) {
// show error
}
},
builder: (context, state) {
return Button(
text: 'Add',
onTap: () {
final data = _textController.text;
context.read<PostDataCubit>().post(data);
});
},
),

Related

Please comment on 3 flutter_bloc writing styles: BlocBuilder, BlocListener, BlocConsumer

I am practicing with flick_bloc and I wonder when to use BlocBuilder, when to use BlocListener and when to use BlocConsumer. I asked a few people, they said that BlocBuilder is used the most and I also started and just practiced with it, but it seems that Blocbuilder only changed for the first time, I don't know if it's true. Can you guys give me some comments on these spellings
Bloc Builder
Used for building widgets, For Example: If you want to show a list of employee names on a page, you can return a ListView widget based on the bloc state. Also, if the employee list comes from an API, then you will need different states such as Loading, Success and Failure states. Based on these different states you can return different widgets from BlocBuilder. A CircularProgressIndicator for showing loading state, ListView for showing employee list in the success state and Error text widget for showing error message if the API fails.
BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
)
Bloc Listener
BlocBuilder can only return widgets. If you want to show a snackbar or want to Navigate from one page to another, then you need to use BlocListener for that.
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
}
)
Bloc Consumer
If you have the use of both BlocListener and BlocBuilder, then it is better to use BlocConsumer. It reduces the boilerplate of using BlocListener and BlocBuilder together.
Code Without Bloc Consumer:
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
},
child: BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
),
)
Code using Bloc Consumer:
BlocConsumer<BlocA, BlocAState>(
listener: (context, state) {
if (state is Success) {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
}
},
builder: (context, state) {
if (state is Loading) {
return CircularProgressIndicator();
}
}
)
BlocBuilder: You can use it to just build out your widgets, but the draw back is that you can't build in Snackbars or Dialogs into the flow, because you must return a widget in blocbuilder and you don't want to return a snackbar or dialog.
BlocListener: This would permit you to use your dialogs and snackbars, but the issue is that it can't let you do anything a blocbuilder would let you do. Which is as you might have guessed, is to return a widget, it's more suited for dismissible UI components like the dialogs and snackbars.
BlocConsumer: This widget helps you combine both a BlocListener and a BlocBuilder, so you can return static components and dismissible UI components.
So if you won't need Snackbars or Dialogs, use a BlocBuilder, If you need Snackbars or Dialogs, use a BlocListener. If you want both of them to work in synergy use a BlocConsumer.
BlocBuilder
This is used when we want to draw a Widget based on what is the current State. In the following example a new “text” gets drawn every time the state changes.
Sample Example
BlocBuilder<OrdersBloc, OrdersState>(
buildWhen: (context, state) {
return state is OrdersState.OrderCompleted
},
builder: (context, state) {
if (state is OrdersState.OrderCompleted) {
return Container(child: Text('Order Completed!'));
} else if (OrdersState.OrderInProgress) {
return Container(child: Text('In Progress'));
} else if (OrdersState.OrderRequested) {
return Container(child: Text('A customer placed an order!'));
} else {
return Container(child: Text('Waiting for an order'));
}
},
);
BlocListener
This is just a listener not a builder (like the above), that means that its job is keep listening for new changes in the state and not to return a widget. You can use listener when you want to show any dialog or any toast, or navigation from one page to another(these are few examples).
Sample Example
BlocListener<OrdersBloc, OrdersState>(
listenWhen: (context, state) {
return state is OrdersState.OrderCompleted;
},
listener: (context, state) {
// Navigate to next screen
Navigator.of(context).pushNamed('OrderCompletedScreen');
},
child: Container(child: Text('Always draw this text!')),
);
BlocConsumer
This is used when we want to draw something based on the current state and execute some actions depending on the new arriving states. This is a mix between “BlocListener” and “BlocBuilder”.
Sample Example
BlocConsumer<OrdersBloc, OrdersState>(
listenWhen: (context, state) {
return state is OrdersState.OrderCompleted ||
state is OrdersState.OrderRefunded;
},
listener: (context, state) {
if (state is OrdersState.OrdersCompleted) {
// Navigate to next screen
Navigator.of(context).pushNamed('OrderCompletedScreen');
} else if (state is OrdersState.OrderRefunded) {
// Report to analytics
Analytics.reportRefunded(state.orderId);
}
},
buildWhen: (context, state) {
return state is OrdersState.OrderCompleted ||
state is OrdersState.OrderInProgress ||
state is OrdersState.OrderRequested;
},
builder: (context, state) {
if (state is OrdersState.OrderCompleted) {
return Container(child: Text('Order Served!'));
} else if (OrdersState.OrderInProgress) {
return Container(child: Text('In Progress'));
} else {
return Container(child: Text('No State'));
}
},
);

Why is Bloc skipping this emit command?

I have an bloc that receives an event called OpenTourStop that invokes a function whose first line of code invokes emit(Waiting()); and then proceeds to execute some logic before emitting a different state. In the UI, the BlocConsumer is supposed to print out a message to the console as soon as state equals Waiting, but it NEVER does. The bloc does not emit the Waiting state, but does emit the other states that result from completing the function. What am I doing wrong?
Below are the relevant sections of code for the bloc and UI
Bloc code:
class QuiztourManagerBloc
extends Bloc<QuiztourManagerEvent, QuiztourManagerState> {
final QuiztourRemoteData _repo;
QuiztourManagerBloc({QuiztourRemoteData repo})
: _repo = repo,
super(QuiztourManagerInitial()) {
on<OpenTourStop>(_openTourStop);
}
_openTourStop(event, emit) {
emit(Waiting()); // Why doesn't the Waiting state show up in the UI?
final _tourStopIndex = event.tourStopIndex;
// section of code removed for clarity
if (_quizPlayDoc.seenRules && tourStopGameResults.isEmpty) {
emit(ShowQuizQuestionViewManager(
quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
// emit(ShowQuizQuestions(quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
} else if (tourStopGameResults.length > 0) {
emit(ShowQuizTourStopScreen(
tour: event.tour,
tourStopIndex: event.tourStopIndex,
quizPlayDoc: _quizPlayDoc,
maxTourStopPoints: _maxTourStopPoints.toString(),
pointsEarned: _tourStopScore.toString(),
));
} else {
emit(ShowQuizRules(_quizPlayDoc));
}
}
}
UI code (from class QuizTourStopViewManager) :
#override
Widget build(BuildContext context) {
return BlocConsumer<QuiztourManagerBloc, QuiztourManagerState>(
builder: (context, state) {
if (state is Waiting) {
print('!!!!!!!!!!!!!!!!!!!!! Waiting '); // Why does this line never get executed?
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
else if (state is ShowQuizTourStopScreen) {
return QuizTourStop( );
}
},
listener: (_, state) {},
);
}
The UI that triggers the event is a button. The code associated with that button is below:
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
if (tourType == "quizTour") {
BlocProvider.of<QuiztourManagerBloc>(context)
.add(OpenTourStop(
tour: tour,
tourStopIndex: selectedTourStopIndex,
));
return QuizTourStopViewManager(
tour: tour,
// game: widget.game,
selectedTourStopIndex: selectedTourStopIndex,
);
I guess that when you send 'OpenTourStop' event at 'onTap' method, 'QuizTourStopViewManager' page is not builded.
So would you try to change event call position after 'OpenTourStop' page is builded?
At initState() method inside.
or
At 'bloc' parameter in BlocConsumer method.

Flutter bloc - Show snackbar on state change

I am trying to log in with Google. When googleSignInAccount I yield new state with PlatformExecption. Now, How can I access that value in order to do some changes.
try{
final GoogleSignInAccount googleSignInAccount = await googleSignIn.signIn();
if(googleSignInAccount == null){
yield GoogleLoginErrorState(error: PlatformException(code: 'user-cancel'));
}
}
my state.dart
class GoogleLoginErrorState extends GoogleLoginState {
final PlatformException error;
GoogleLoginErrorState({this.error});
#override
List<Object> get props => [error];
}
my BlocBuilder
if (state == GoogleLoginErrorState()) {
}
For side effects such as showing snackbars / dialogs or navigating to another screen, you have to use BlocListener, something like this:
BlocListener<YourGoogleSigninBloc, YourGoogleSigninState>(
listener: (context, state) {
if(state is GoogleLoginErrorState){
// show snackbar here
}
},
child: YourWidget(),
)
you can also use BlocConsumer instead of nesting BlocListeners and BlocBuilders like this:
BlocConsumer<YourGoogleSigninBloc, YourGoogleSigninState>(
listener: (context, state) {
if(state is GoogleLoginErrorState){
// show snackbar here
}
},
builder: (context, state) {
return YourWidget();
},
)
In the bloc builder,
if (state is GoogleLoginErrorState) {
}
This checks if the state data type is GoogleLoginErrorState
And use the state
Text(state.error.code)

Flutter Bloc consumer states overlap

I'm having some troubles with the bloc pattern, specifically with BlocConsumer reactions to the emitted states which lead to certain overlaps.
The main character is an object list, for semplicity let's say an object with a String and int parameters.
On the first page I'm adding an event to the bloc to immediately perform the list fetch:
ExampleBloc _exampleBloc;
#override
void initState() {
super.initState();
_exampleBloc = ExampleBloc()..add(FetchNewList());
}
The layout, a grid view holding buttons with the string param as label follow by the int value, it's build by a BlocConsumer once the list is fetched:
BlocConsumer(
cubit: _exampleBloc,
builder: (context, state) {
if (state is ListLoadingState) {
return CircularProgressIndicator();
} else if (state is ListLoadedState) {
return GridView.count(
padding: const EdgeInsets.all(20),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
crossAxisCount: 2,
children: state.exampleList
.map((e) => FlatButton(
onPressed: () => _openSecondScreen(),
child: Text('${e.label} ${e.value}'),
))
.toList());
} else if (state is ListFetchErrorState) {
return FlatButton(
onPressed: () =>
_exampleBloc = ExampleBloc()..add(FetchNewList()),
child: Text('try again'));
} else {
return Container();
}
},
listener: (context, state) {
//will be used for other purpose
},
),
Non-blocking errors (for problems on list edit operations basically) are shown in a snackbar.
Clicking on a button open a second page, where the same list is loaded as a ListView and some actions allows making changes on list items (for simplicity sake a tap increment the int value of 1 while a long click delete the item from the list):
BlocConsumer(
cubit: _exampleBloc,
builder: (context, state) {
if (state is ListLoadedState) {
return ListView.builder(
itemCount: state.exampleList.length,
itemBuilder: (BuildContext context, int index) {
var item = state.exampleList[index];
return ListTile(
onTap: () => _exampleBloc.add(UpdateItemEvent(
Object(item.label, item.value + 1))),
onLongPress: () =>
_exampleBloc.add(DeleteItemEvent(item)),
title: Text(
'${item.label} + value: ${item.value.toString()}'),
);
});
} else {
return Container();
}
},
listener: (context, state) {
if (state is ListUpdateErrorState) {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage)),
);
} else if (state is ListUpdateInProgressState) {
_showCircularProgressIndicator();
}
},
));
First problem: as the list is the same for both pages, I thought it would be ok to use the same bloc but, even if the last state on the first page is the "ListLoadedState", once the second page is loaded the bloc builder is not building the corresponding widget. As workaround I thought to store the fetched list in a variable in the bloc class and adding an event (GetListAlreadyLoaded) in the initState of the second page to force the bloc to emit once again the ListLoadedState holding the list. Is there a better way to retrieve the last state of the bloc?
Second problem: if any error occurs while performing an update/delete operations on the second page, I would like to simply show a snackbar with an error message. So in the bloc class I have something like this:
#override
Stream<ExampleState> mapEventToState(ExampleEvent event) async* {
//more events...
} else if (event is PerformListUpdate) {
yield ListUpdateInProgressState();
//performing remote update
var updateResult = await _repository.performUpdate(event.objectToUpdate)
//updating local list
if (updateResult.isSuccessful) {
var index = exampleList.indexWhere(
(element) => element.label == event.objectToUpdate.label);
exampleList[index] = event.objectToUpdate;
//triggering the bloc builder with the updated list
yield ListLoadedState(exampleList);
} else {
//emit a state in order to show the error message
yield ListUpdateErrorState(
"An error occurred while performing update");
}
}
}
}
The problem is that if the ListUpdateErrorState is emitted the snackbar is shown, but the bloc builder is triggered by the new state and it rebuilds the widget in the else branch, which is an empty container. As a workaround I thought to first emit the ListUpdateErrorState to allow the listener function react and show the snackbar then, soon after, emit again the ListLoadedState with the last list value in order to trigger also the builder and show again the list view. Is that okay or there's a better way to show errors?
Third problem (basically the same as the second): while performing an asynchronous operation on the second page I would like to show some CircularProgressIndicator without "losing" the list view which could be for example in the appbar, at the bottom of the list or in the middle of the screen above the list. Emitting the "ListUpdateInProgressState" while starting the operation and reacting to it in the bloc listener however triggers the builder function which "destroys" the List view. How can I show the loading indicator without losing the list view?

Delay state check inside Flutter bloc listener

I'm sending data to the server via a bloc and showing a progressSnackBar during, and then a successSnackBar on success. Sometimes this takes less than a second and it makes sense to not show the progressSnackBar at all - in other words wait a second and then check if the state is still UpdatingAccount. I've tried and failed with different combinations involving Future.delay(...) and I can probably do a setState hack but is there a way to achieve this just inside the bloc listener?
BlocListener<AccountBloc, AccountState>(
listener: (BuildContext context, state) {
if (state is UpdatingAccount) { // <-- delay this
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(progressSnackBar());
} else if (state is AccountUpdated) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(successSnackBar());
}
},
// rest...
),
I ended up making the widget stateful and giving it an _updated bool member.
BlocListener<AccountBloc, AccountState>(
listener: (BuildContext context, state) {
if (state is UpdatingAccount) {
_updated = false;
Future.delayed(Duration(seconds: 1), () {
if (!_updated) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(progressSnackBar());
}
});
} else if (state is AccountUpdated) {
_updated = true;
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(successSnackBar());
}
},
// rest...
),
You can do Future.delay() in your state is UpdatingAccount condition and check state again.
if (state is UpdatingAccount) {
Future.delayed(Duration(seconds: 1), (){
if(state is "UpdatingAccount"){
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(progressSnackBar());
}
});
}