Flutter Bloc consumer states overlap - flutter

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?

Related

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.

Riverpod showing snackbar on Error and also last known list using statenotifier

I am using riverpod ^1.0.0. I have created a StateClass which extends Equatable. In my StateNotifier i set state depending on events and outcomes. One being an async http request which upon success sets
state=SalesOrderListSuccess(salesOrderListItems: _items);
Upon http client failure however i set state to:
state = SalesOrderListError(error: response.data);
This works, upon success it renders the list in below UI builder. And it also using ref.listen and shows the snackbar. However, because the state changes from SalesOrderListSuccess and i am using ref.watch it seems that it cant keep the former known list and UI. How can i show the snackbar above the last known SalesOrderListSuccess/UI without rendering an entire new Error Page that is empty of all the items i have already managed to render in the list ?
Basically i dont want the list to change, just show a snackbar above last known list before the http client error happend.
Here the current widget: (this requires the SalesOrderListSuccess state in order to show the list).
#override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
final selectedtrack = ref.read(selectedProductIdProvider2.notifier);
ref.listen(todoListProvider, (previous, count) {
print(previous);
print(count);
switch (count.runtimeType) {
case SalesOrderListError:
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ohh no some error happend')),
);
}
});
return ListView.builder(
shrinkWrap: true, // 1st add
physics: ClampingScrollPhysics(),
itemCount: (todos as SalesOrderListSuccess).salesOrderListItems.length,
itemBuilder: (context, index) {
final current=
(todos as SalesOrderListSuccess).salesOrderListItems[index];
return ListTile(
title: Text('${current.title}'),
onTap: () {
selectedtrack.state = index;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(),
),
);
});
},
);
}
Hi have a look at riverpod_messages.
I had your same problems and I have written a package for this
https://pub.dev/packages/riverpod_messages/versions/1.0.0
Let me know!

BlocListener not being executed after Cubit function call

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);
});
},
),

Fixing Issues with FutureBuilder

In my Flutter project, I am trying to implement a button click event by using FutureBuilder. Basically when the button clicked, it supposed to get the data and display in a table. So my button onPressed event handling is as below:
onPressed: () async{
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
var p = double.parse(loanAmount);
var r = double.parse(interestRate);
var n = int.parse(monthes);
Api api = new Api();
new FutureBuilder<List>(
future: api.calculateEmi(p, r, n),
builder: (BuildContext buildContext, AsyncSnapshot<List> snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
} else {
print( snapshot.data);
return new SingleChildScrollView(
padding: const EdgeInsets.all(8.0),
child: DataTableWidget(listOfColumns: snapshot.data.map(
(e)=>{
'Month': e['Month'],
'Principal': e['Principal'],
'InterestP': e['InterestP'],
'PrinciplaP': e['PrinciplaP'],
'RemainP': e['RemainP']
}).toList()
),
);
}
}
);
}
}
The Api call is working and the method calculateEmi is called and get data returned ( a List of Map), but the view just not updated and no table appeared at all, and I use breakpoint at the builder portion but it never go into it, where did I do wrong, can anyone help? thanks.
The FutureBuilder needs to be inserted somewhere in the flutter widget tree. Simply creating a new FutureBuilder doesn't tell flutter what to do with it.
I'm guessing you instead want to put the FutureBuilder you created somewhere in the parent widget of the onPressed method. If you need it to only show when the button is pressed you can do that with a bool that determines whether to show the FutureBuilder or not.
Ex.
Widget build(context) {
if(buttonPressed) {
return FutureBuilder(
...
);
}
else {
return Container();
}
}

BlocBuilder partially update ListView

Project
Hi, I'm trying to use a bloc pattern to create a list view that can be filtered by a TextField
Here is my code
bloc:
class HomeBloc extends Bloc<HomeEvent, HomeState> {
List<Book> displayList = [];
....
#override
HomeState get initialState => UnfilteredState();
#override
Stream<HomeState> mapEventToState(
HomeEvent event,
) async* {
....
//handle filter by input
if(event is FilterListByTextEvent) {
displayList = displayList.where((book){
return book.title.toLowerCase().contains(event.filterString.toLowerCase());
}).toList();
yield FilteredState();
}
}
}
view
class BookList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
final HomeBloc bloc = Provider.of<HomeBloc>(context);
print(bloc.displayList);
return ListView.builder(
shrinkWrap: true,
itemCount: bloc.displayList.length,
itemBuilder: (context, index) {
return Dismissible(
key: UniqueKey(),
background: Container(
color: selectedColor,
),
child: Container(
height: 120,
padding: const EdgeInsets.fromLTRB(20, 4, 20, 4),
child: BookCard(
book: bloc.displayList[index],
),
),
onDismissed: (DismissDirection direction) {
},
);
},
);
},
);
}
}
Problem
I've read some other discussion about bloc pattern and List view but the issue I'm facing here is different.
Every time my Filter event is called, bloc correctly generate a new displayList but, when BlocBuilder rebuild my UI, listview is not correctly updated.
The new filtered list is rendered but old results do not disappear. It seems like the new list is simply stacked on top of the old one.
In order to understand what was happening I tried printing the list that has to be rendered, inside the BlocBuilder before the build method is called.
The printed result was correct. In the console I see only the new filtered elements while in the UI I see both the new one and the old one, one below the other.
What am I missing here?
Thanks
Keep an intermediate event, eg. a ListInit for which you will display a CircularProgressIndicator. BlocBuilder will be stuck on previous state unless you update it over again.
So in your bloc, first yield the ListInit state and then perform filtering operations, and then yield your FilteredState.
Make new state for loading and yield before displayList.
if(event is FilterListByTextEvent) {
yield LoadFilterList();
displayList = displayList.where((book)
{
return
book.title.toLowerCase().contains(event.filterString.toLowerCase());
}).toList();
yield FilteredState();
}