Description
Description
We often use the BlocConsumer or BlocListener to deliver single-based events to the UI (such as: showing snackbar or dialog). On the BlocConsumer we uses the listenWhen and buildWhen methods to differentiate between such events. In the following example I show a Cubit and the corresponding implementation that we normally use:
//State class
abstract class NewsState {}
//screen based states
sealed class NewsScreenState extends NewsState {}
class NewsScreenLoadingState extends NewsScreenState {}
class NewsScreenDataState extends NewsScreenState {
final List<String> items;
NewsScreenDataState(this.items);
}
//single event based states
sealed class NewsEventState extends NewsState {}
class NewsEventLoadingError extends NewsEventState {}
class NewsEventShowInfo extends NewsEventState {
final String info;
NewsEventShowInfo(this.info);
}
//Cubit class
class NewsCubit extends Cubit<NewsState> {
NewsCubit(this._repo) : super(NewsScreenLoadingState()) {
_loadData();
}
final NewsRepository _repo;
void _loadData() async {
final data = await _repo.getNews();
emit(NewsScreenDataState(data));
}
void showInfo(String id) async {
final data = await _repo.getInfo();
if (data.isSuccess) {
emit(NewsEventShowInfo('some data'));
} else {
emit(NewsEventLoadingError());
}
}
}
//Widget implementation
class NewsWidget extends StatelessWidget {
const NewsWidget({super.key});
@override
Widget build(BuildContext context) {
return BlocConsumer<NewsCubit, NewsState>(
listenWhen: (previous, current) => current is NewsEventState,
buildWhen: (previous, current) => current is NewsScreenState,
listener: (context, state) => switch (state as NewsEventState) {
NewsEventLoadingError() => context.showSnackbar('something went wrong'),
NewsEventShowInfo(info: final info) => showDialog(
context: context,
builder: (context) => AlertDialog(title: Text(info)),
),
},
builder: (context, state) => switch (state as NewsScreenState) {
NewsScreenLoadingState() => const Center(
child: CircularProgressIndicator(),
),
NewsScreenDataState(items: final items) => ListView.builder(
itemBuilder: (context, index) => ElevatedButton(
onPressed: () => context.read<NewsCubit>().showInfo(items[index]),
child: Text(
'$index click me!',
),
),
itemCount: items.length,
),
},
);
}
}
This works fine when the bloc/cubit is responsible for the current emitted state. But if the user clicks the item and for example the "NewsEventShowInfo" state is emitted and then flutter decide to rebuild the widget we got an error because the "buildWhen" method is ignored and the switch expression of the builder method fails because the last emitted state "NewsEventShowInfo" is not a instance of the sealed NewsScreenState class.
What would be the correct procedure here? The best approach would be if the BlocConsumer implementation would emit (on rebuild) not the last emitted state but the last emitted state which passed the buildWhen method.
Activity
felangel commentedon Oct 4, 2024
Hi @bobekos 👋
Thanks for opening an issue!
Are you able to share a link to a complete, minimal reproduction sample that illustrates the problem you're facing? I'm not sure I fully understand the problem since the error SnackBar is shown as part of the
listener
, not thebuilder
and it should only ever be called on state changes (independent of rebuilds). Thanks!bobekos commentedon Oct 5, 2024
Hi @felangel glad you found time to answer me. I prepare a full example of exactly what i mean and my view of things.
Here is a "full" example to reproduce what i mean (i would suggest to run this on web, mac or windows to better trigger the rebuild):
If you now start this example and resize the window back and forth in width, everything works. But if you click on a button and then trigger the rebuild (by modifying the width), the cast in the builder method of the BlocConsumer widget fails.
And of course I know what happens here and that it is not an error of bloc in that particular sense. The last known state is of the type NewsEventState. So this state is passed to the BlocConsumer and since it has no other state, it must of course be able to render something, ignores the "buildWhen" method and the cast fails.
My solution now, I have changed the structure and such “One Time Events” (like NewsEventState in the example) are now emitted to the UI independently of the main “bloc stream”. For this I use the package bloc_presentation which provides an additional stream for such type of events.
But I would like to know how you would approach such a problem and what the structure of your state class would look like. I'm not too happy with the options I've come up with like:
In my opinion also not a satisfying solution and it breaks the idea behind the bloc implementation and the "state" keeping. On the other hand it solves the problem with the one time events.
I would like to know how you would handle such things like this?
xoliq0v commentedon Nov 17, 2024
Hi! 👋
Thanks for highlighting this issue. I see how this can lead to errors when the buildWhen condition is ignored on a rebuild, especially if the last emitted state is an instance of NewsEventState, which isn’t intended for building the UI.
Suggested Workaround:
Until a more robust solution is implemented, you could consider an approach like maintaining two different streams or creating a “placeholder” screen state after each event is emitted. Here’s an idea to ensure that the last screen-based state persists, even after an event-based state:
In this case, lastScreenState would hold the most recent screen state, ensuring that when an event state occurs, it won’t interfere with the builder logic upon a rebuild.
Proposed Enhancement:
It would indeed be helpful if BlocConsumer could track the last emitted state that passed the buildWhen condition and only rebuild based on that state. This would allow BlocConsumer to separate the responsibilities of listener and builder more effectively, avoiding this kind of rebuild conflict.
Would be glad to assist with an implementation or further discussion if needed. Thanks again for bringing this up!
bobekos commentedon Nov 20, 2024
Hi @xoliq0v
thank you for your response. I mentioned the same workaround that you suggested in the first point of my comment above. But like you, I don't like this solution at all. Firstly, you have to think about it and secondly, this implementation leads to unnecessary rebuilds of the widget.
I'm glad that you see it the way I do. If there is still a need here, I would be happy to help as far as my expertise allows.