It seems like bug in bloc v0.11.2
I have the following Event/State:
class DeleteReceipt extends ReceiptEvent {
final Receipt receipt;
DeleteReceipt(this.receipt) : super([receipt]);
}
class ReceiptDeleted extends ReceiptState {
final Receipt receipt;
ReceiptDeleted(this.receipt) : super();
}
and the following code in bloc:
if (event is DeleteReceipt) {
var delReceipt = event.receipt;
await _receiptDao.delete(delReceipt);
print("deleting: " + delReceipt.snapshot.documentID);
yield ReceiptDeleted(delReceipt);
}
and my widget I have:
if (state is ReceiptDeleted) {
print("delete: "+state.receipt.snapshot.documentID);
receipts.delete(state.receipt);
}
and when I do: _receiptBloc.dispatch(DeleteReceipt(receipt));
the first time I get:
I/flutter (28196): deleting: AzgAzcn5wRNFVd7NyZqQ
I/flutter (28196): delete: AzgAzcn5wRNFVd7NyZqQ
which is correct, but the second time I do _receiptBloc.dispatch(DeleteReceipt(receipt)); on a different receipt, I get:
I/flutter (28196): deleting: d4oUjrGwHX1TvIDr9L2M
I/flutter (28196): delete: AzgAzcn5wRNFVd7NyZqQ
You can see that in the second time the DeleteReceipt event was received with the correct value, but the ReceiptDeleted State was received with the wrong value, and then it just get stuck like this, it never fires ReceiptDeleted State with the correct value, only with the first value.
My app is not trivial, and I have set many events and state in the past, and it worked with no issue (except this one, that probably is related flutter bloc state not received)
Basically I let the user create photos of receipt, that are persistent (using bloc/firestore), and I want to let the user delete them, so when the user click on a receipt, it opens in a new screen:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return ReceiptDetailPage(widget.receipt);
},
),
and when the user click on delete, I show a dialog, and delete the receipt if is OK
var result = await showDialog(
context: context,
builder: (BuildContext dialogCtxt) {
// return object of type Dialog
return AlertDialog(
title: new Text(AppLocalizations.of(context).deleteReceiptQuestion),
actions: <Widget>[
// usually buttons at the bottom of the dialog
new FlatButton(
child: new Text(AppLocalizations.of(context).cancel),
onPressed: () {
Navigator.of(dialogCtxt).pop("cancel");
},
),
new FlatButton(
child: new Text(AppLocalizations.of(context).ok),
onPressed: () {
Navigator.of(dialogCtxt).pop("OK");
},
),
],
);
},
);
if (result == 'OK') {
Navigator.of(context).pop();
_receiptBloc.dispatch(DeleteReceipt(receipt));
}
Solution:
add state/event:
class EmptyState extends ReceiptState {}
class EmptyEvent extends ReceiptEvent {}
after receiving the delete state do:
if (state is ReceiptDeleted) {
print("delete: "+state.receipt.snapshot.documentID);
receipts.delete(state.receipt);
_receiptBloc.dispatch(EmptyEvent()); // add this line
}
and add this to your bloc
if (event is EmptyEvent) {
yield EmptyState();
}
This will cause an empty event and state to be fired and will clear the problem
Explain: I noticed that once I fire a State, the block provider will send that state every time I change a screen, which is strange since the app is receiving a Delete State many time. this is not a problem in my case, since the code will try to delete an element that is already delete and will fail quietly:
void delete(Receipt receipt) {
try {
Receipt oldReceipt = receipts.firstWhere(
(r) => r.snapshot.documentID == receipt.snapshot.documentID);
receipts.remove(oldReceipt);
} catch (e) {
print(e);
}
}
NOTE: this seems to happen with all State that the app is firing, not only the Delete state
So I guest that if I will fire an empty event, it will clear the old Delete state, and will somehow fix the issue, and WALLA...
Note that I didn't had to actually listen to the EmptyState any where in my code
MORE INFO:
I realize that although the bloc seems to loose state, also my design is wrong, because the Data Structure should be updated in the bloc, once the event is received and not in the widget, when the state is received (or not received in this case, which cause the bug)
Initially I used bloc with sembast, but then I wanted the data to be sync with the remote DB, so I replaced sembast with firestore.
but that causes the load time to go from nothing, to more than 2 seconds, and that is a problem since in the original design I load all the data from the DB on every update.
So I tried to update the store and the UI seperatly, ie. instead of reading all the data, I keep a List in my widget and update the widget when the state changes - per update/delete state.
That was a problem, since many state were lost (especially when the user click fast - which cause many events/states to fire)
So I guess a correct solution would be to manage the in-memory Data in a separate Service, and update the Data when the Event is received, and then read all data from the Service instead of the store (when possible)
Related
I'm implementing a BLoC pattern for state management in my Fluter application. As I'm new in Flutter and BLoC particularly I'm evolving its usage gradually.
For new I use BLoC to communicate between two pages. One page sends an asset to the BLoC and navigates to details page. The details page uses StreamBuilder to read from the BLoC and build page with according data:
AppWidget:
Widget build(BuildContext context) => MultiProvider(
providers: [
BlocProvider(create: (context) => AssetBloc())
...
Requesting page
_onAssetMenuAction(BuildContext context, AssetMenu value, Asset asset) {
switch (value) {
case AssetMenu.validate:
var bloc = BlocProvider.of<AssetBloc>(context);
bloc.validate(asset);
Navigator.push(context,
MaterialPageRoute(builder: (context) => ValidateAssetPage()));
break;
}
Validation page
Widget build(BuildContext context) {
var bloc = BlocProvider.of<AssetBloc>(context);
Logger.root.info("Building validation page");
return StreamBuilder<AssetValidation>(
stream: bloc.outValidation,
builder: (context, snapshot) => snapshot.hasData
? QrImage.withQr(qr: snapshot.data!.qr)
: Text("No QR"));
}
BLoC
class AssetBloc extends BlocBase {
//
// Stream to handle the validation request outcome
//
StreamController<AssetValidation> _validationController =
StreamController<AssetValidation>.broadcast();
StreamSink<AssetValidation> get _inValidation => _validationController.sink;
Stream<AssetValidation> get outValidation => _validationController.stream;
//
// Stream to handle the validation request
//
StreamController<Asset> _validateController = StreamController<Asset>();
void Function(Asset) get validate => _validateController.sink.add;
//
// Constructor
//
AssetBloc([state]) : super(state) {
_validateController.stream.listen(_handleLogic);
}
void _handleLogic(Asset asset) {
_inValidation.add(AssetValidation.create(asset));
Logger.root.finest("AssetValidation instance is sent to stream");
}
void dispose() {
_validateController.close();
_validationController.close();
}
}
The problem I have is I'm getting "No QR". According to logs I see following sequence of actions:
new AssetValidation.create(): Validating asset Instance of 'Asset'
AssetBloc._handleLogic(): AssetValidation instance is sent to stream
ValidateAssetPage.build(): Building validation page
So at the moment of validation page building the validation result data should be in the stream but it seems they are not.
Unit tests of AssetBloc work as expected. So I suspect it should be related to StreamBuilder in validation page.
The StreamBuilder just shows you the last value of the stream whether the StreamBuilder was present on the current deployed widget when the stream was updated. So, if you add a new value to the stream, but the StreamBuilder is not on the current deployed widget, and, after that, you deploy the widget with the StreamBuilder, it's very likely that it won't show the updated data (in fact it shows empty data). I know, it's weird, i have the same problem when i like to use streams in that way. So, instead, i recommend you to use ValueListenable on the bloc and ValueListenableBuilder on the widget. It's very useful for that cases.
Another thing to point out is that if you're going to use just streams for the state management, it's better to use another state manager type such as provider or singleton. The reason is that, the right way to use bloc (the way you take advantage of the power of bloc) is using just the method add() for the events and logic, and using the established bloc State classes to show and update the data with the BlocBuilder on the widget.
I have several sub-screens which give the user the option to save some data. When that screen is closed, I want the parent screen, which pushed the sub-screen, to know whether data was saved or not. The sub-screens maintain a didSave flag and are set to true when data is saved.
There are several ways for the sub-screens to be closed:
hardware/software back button.
The close button on the AppBar.
A button on the screen itself.
I can handle the 3rd case using Navigator.pop(context, didSave) and in the parent that didSave flag is captured using final didSave = await Navigator.push<bool>(context, myRoute).
However, for the first 2 cases, the result will obviously be null.
I have looked into WillPopScope but that only is used to determine whether the screen should be closed or not. It doesn't seem that I can set any data to be returned for the push call.
I have also looked into wrapping the parent screen in a Provider where it can listen to didSave states but that will trigger immediately when emitted which is not desirable for my use case. I only want to act when the sub-screen is closed not when data is saved.
I can potentially use WillPopScope and emit an event there if a save operation has occurred but I would like a simpler solution if available.
Is there a way for this that I am missing?
Thanks!
as you said the Provider will listen to didSave and this doesn't fit in your case you can use just a simple inheritedWidget:
wrapping the parent like this:
InheritedExampleWidget(
didSave: false,
child: Parent(),
),
you need to set a setter to didSave
then on the ascendant widgets on the widget tree, you can:
InheritedExampleWidget.of(context).didSave = true;
this will not trigger it immediately, which the Provider package solves.
then you can manage the state however you want
Did you try to create the variable with state management? And there is method with the push that do task after user come from the child screen. So, when they come you can checkout the use have saved data or not.
For EG:
saved = false; //State management variable
//We are pushing on another screen.
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) =>
new ScreenName(),
),
).then((val) {
//This part with work when user popped the screen on which we have pushed
if (!saved) {//Do Something when not saved}
});
Try above code and let me know if you get any error or you're facing any issue.
when you push a new route, using StatefulWidget, it will have a lifecycle starting from an createState, when the widget isn't there on the widget tree ( when we pop ), the dispose method will be called.
those cases of popping the route:
hardware/software back button.
The close button on the AppBar.
A button on the screen itself.
will trigger the dispose method to execute.
so you can put inside it your logic that you want.
exmaple :
class classTest {
bool didSave = false;
}
then when on the property where you want to push the screen set it to that classTest's didSave, as an example:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const YourRouteWidget(didSave: classTest.didSave,
),
),
);
on that point it's false, then when the user will complete using the screen, going back with any king of popping the route (with Navigator.pop(context), or with back button...), the dispose method will be called, so you can :
void dispose() {
if(/* your conditions*/ ) {
classTest.didSave = true;
}
super.dispose();
}
it will be back with a true value to the parent page.
I'm working on a flutter project and all of a sudden, the bloc builder I was using stopped recognizing a specific state change. Here is an example of the code
class ProfileBloc extends HydratedBloc<ProfileEvent, ProfileState>
...
on<ProfileSelected>(_mapProfileSelectedToState);
...
FutureOr<void> _mapProfileSelectedToState(
ProfileSelected event, Emitter<ProfileState> emit) async {
emit(state.copyWith(
profileSelectionStatus: ProfileSelectionStatus.selecting,
));
setHeaders();
setRedirect();
emit(state.copyWith(
selectedProfile: event.selectedProfile
profileSelectionStatus: ProfileSelectionStatus.selected,
));
}
...
BlocBuilder<ProfileBloc,ProfileState>(builder: (context, profileState) {
return profileState.profileSelectionStatus == ProfileSelectionStatus.selected ? Text("Profile Selected") : Text("Profile Not Selected");
}
...
when the BlocBuilder runs, it never registers the ProfileSelectionStatus.selected state. It always returns Profile Not Selected. That is until I refresh the browser, then it reports as Profile Selected. If I wrap the second emit(the profileSelected one) in the bloc in a Future.delayed(Duration.zero,()=>emit...), then the bloc builder works as expected returning Profile Selected after it's emitted.
The thing is, it worked for a long time, then without changing those two blocs of code, it stopped working. Any ideas on what's wrong?
Web application contains many screens with list views and edit dialogs for edit selected records. List view uses "standard display behavour" using StreamBuilder.
Simplified bloc.dart
class Bloc {
final _subject = StreamController<Data>.broadcast();
Stream<Data> get data => _subject.asyncMap(_getData);
Future<Data> _getData(...) {
try {
final data = await api.getUrl(...);
return data;
} on SomeException catch (e) {
// This exception is catched by StreamBuilder
throw ApiException('could not get data');
}
}
}
Simplified widget.dart
StreamBuilder<...>(
stream: bloc.data,
builder: (context, snapshot) {
if (snapshot.hasError) {
// Bloc throws an exception which thrown by API client (HttpClient).
return ErrorWidget(snapshot.error.toString());
}
if (snapshot.hasData) {
return ListView.builder(...);
} else {
return CircularProgressIndicator();
}
}
),
Each edit dialog uses showDialog to display selected item details and save modifications.
There may be a situation when (for example) access token is invalid (or expired) during his already authenticated session. It is necessary to inform user about this (showing him a warning) and after closing warning destroy the session and return to login screen. Is it possible to implement effective solution to catch specific exception (eg. TokenInvalidException) in a single place without adding symbolic code like:
if (snapshot.error is TokenInvalidException) {
// destroy session
// return to Login
}
into each StreamBuilder list view and some code to check if exception occurred on each Dialog's save action?
I have 100500 screens and I do not want to implement the same code for each screen. Actually I want some global exception catcher which catches the required exception and executes specific action.
Now I see some global StreamController with stream which accepts such exception (events) and is listen by some global parent widget (MaterialApp or maybe Main).
final globalExceptionController = StreamController<TokenInvalidException>.broadcast();
// somewhere
globalExceptionController.stream.listen((e) async {
// Show warning
final result = await showDialog(...);
// Go to login
Navigator.pushReplacement(context, <login page route>);
}) {}
The problem I see here is the bloc object which throws the exception will have strong dependency on stream controller which is not interested to it absolutely. But actually I need to link somehow the bloc and GUI because to show warning and switch the route I need a context.
Any ideas or critics are appreciated.
Maybe the coordinator bloc pattern described here helps you with this problem
I have 2 screens, the first screen is to display a list of items. The second one is a form for creating a new item. On the server side, after each store, update, destroy action, I returns its value back to the client as a response. The goal here is to update the state without having to make a new request over the network and showing a loading screen over and over every time the user creating a new resource and going back to the screen containing the list.
But since the BlocBuilder is only depends on a specific event and state.data is only available inside an if (event is NameOfEvent) statement, I couldn't modify the current state to push the new value to it.
I found that using BlocListener combined with InitState and setState is somehow working, but I think it makes the state.data is useless inside the BlocBuilder also making the algorithm complicated for such a small task.
Is there any simpler way to achieve this condition?
child: BlocBuilder<EducationBloc, EducationState>(
builder: (context, state) {
if (state is EducationLoaded) {
return ListEducationData(educations: state.data);
}
if (state is EducationCreated) {
final education = state.data;
// i want to push [education] to <ListEducationData>
}
...
...
Create list to store data and use BlocListener so you don't need to change ui every state changes.
var _listEducations = List<EducationData>();
child : BlocListener<EducationBloc,EducationState>(
listener: (_,state){
if (state is EducationLoaded) {
setState(() { //update _listEducations
_listEducations.addAll(state.data);
});
}
},
child : Container()
)