Why isn't bloc builder recognizing the state change? If I wrap emit in a Future.delayed(Duration.zero) it works fine - flutter

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?

Related

How to attend best practice for not using UI code in the Controller with GetX flutter when I need to show a Dialog if my task complete.?

For a simple Email login with OTP code I have a structure as follows.
View
await _signUpCntrl.signUp(email, password);
Controller
_showOtpDialog(email);
_showOtpDialog func
return Get.dialog(
AlertDialog(
So the thing is _showOtpDialog function is inside a controller file. ie. /Controllers/controller_file.dart
I want do something like a blocListener, call the _showOtpDialog from a screen(view) file on signup success. (also relocate the _showOtpDialog to a view file)
Using GetX I have to use one of the builders either obs or getbuilder. Which is I think not a good approach to show a dialog box.
On internet it says Workers are the alternative to BlocListener. However Workers function resides on Controller file and with that the dialog is still being called on the controller file.
As OTP dialog will have its own state and a controller I wanted to put it inside a /view/viewfile.dart
How do I obtain this?
I tried using StateMixin but when I call Get.dialog() it throw an error.
visitChildElements() called during build
Unlike BLoC there's no BlocListener or BlocConsumer in GetX.
Instead GetX has RxWorkers. You can store your response object in a Rx variable:
class SomeController extends GetxController{
final response= Rxn<SomeResponse>();
Future<void> someMethod()async{
response.value = await someApiCall();
}
}
And then right before the return of your widget's build method:
class SomeWidget extends StatelessWidget{
final controller = Get.put(SomeController());
#override
Widget build(BuildContext context){
ever(controller.response, (SomeResponse res){
if(res.success){
return Get.dialog(SuccessDialog()); //Or snackbar, or navigate to another page
}
....
});
return UI();
}
First thing, you will need to enhance the quality of your question by making things more clearly. Add the code block and the number list, highlight those and making emphasize texts are bold. Use the code block instead of quote.
Seconds things, Depends on the state management you are using, we will have different approaches:
Bloc (As you already added to the question tag). By using this state management, you controller ( business logic handler) will act like the view model in the MVVM architecture. In terms of that, You will need to emit a state (e.g: Sent success event). Afterward, the UI will listen to the changes and update it value according to the event you have emitted. See this Bloc example
GetX (As your code and question pointed out): GetX will acts a little bit different. you have multiple ways to implement this:
Using callbacks (passed at the start when calling the send otp function)
Declare a general dialog for your application ( this is the most used when it comes to realization) and calling show Dialog from Bloc
Using Rx. You will define a Reactive Variable for e.g final success = RxBool(true). Then the view will listen and update whenever the success changes.
controller.dart
class MyController extends GetxController {
final success = RxBool(false);
void sendOtp() async {
final result = await repository.sendOTP();
success.update((val) => {true});
}
}
view.dart
class MyUI extends GetView<MyController> {
#override
Widget build(BuildContext context) {
ever(controller.success, (bool success) {
// This will update things whenever success is updated
if (success) {
Get.dialog(AlertDialog());
}
});
return Container();
}
}

Flutter Navigator popUntil with Bloc not rebuilding state

I have a 3 step form, For each step I am pushing a new page by passing the Bloc provider wrapped in MaterialPageRoute using Navigator so the same bloc can be accessed on those pages like this:
Navigator.of(context).push(MaterialPageRoute<TeacherTemperature>(
builder: (_) => BlocProvider.value(value: BlocProvider.of<TeacherattendanceclassBloc>(context),
child: TeacherTemperature()))
);
Now on the last step, when the form is completed I am doing popUntil to go back to step 1 and update the state:
Navigator.popUntil(context, ModalRoute.withName("teacherAttendanceClassList"));
BlocProvider.of<TeacherattendanceclassBloc>(context)
.add(StudentsCheckedInCompleted(widget.studentList!.length));
This successfully goes back to step 1 page and also I am able to call the event StudentsCheckedInCompleted event and the bloc is processing the state change as well.
But The step one page is not re-building for some reason? (I have tried to but the condition under the BlocBuilder as well)
This is the step one page, where I'm listening for the state in the BlocListener like this:
BlocListener<TeacherattendanceclassBloc, TeacherattendanceclassState>(
listener: (context, state) {
print(state);
if(state is StudentSelectionUpdated){
childrenSelectedList = state.studentsSelectedList;
}
if(state is StudentsCheckedInSuccessfull){ ----> this is not being triggered on the page
print('StudentsCheckedInSuccessfully is triggered on the teacher attendance page');
}
},
Am I missing somethig?
This issue might be caused by State equatibility check, which means:
Assume this is your state;
class MyState extends Equatable {
final String myVariable1;
final int myVariable2;
MyState({this.myVariable, this.myVariable2});
#override
List<Object> get props => [myVariable, myVariable2];
}
on the bottom, as you see, there is props which helps bloc to decide if state is changed or altered, and if that prop say "i am changed" (by changing any content of it), bloc rebuilds the widget. But if your state is not altered/changed as intended or you forgot to add proper props that will later say to bloc that you edited your state, your widget will not be rebuilt.
Maybe you can can add more details in code to understand the root cause. (Can you please share your bloc, events and your state objects)

Riverpod - ref.onDispose called when object is created, and not when disposed

I have this ViewModel and a Riverpod provider for it:
final signInViewModelProvider = Provider.autoDispose<SignInViewModel>((ref) {
final vm = SignInViewModel();
ref.onDispose(() {
vm.cleanUp();
});
return vm;
});
class SignInViewModel extends VpViewModelNew {
FormGroup get form => _form;
String get emailKey => _emailKey;
String get passwordKey => _passwordKey;
final String _emailKey = UserSignInFieldKeys.email;
final String _passwordKey = UserSignInFieldKeys.password;
final FormGroup _form = FormGroup({
UserSignInFieldKeys.email:
FormControl<String>(validators: [Validators.required]),
UserSignInFieldKeys.password:
FormControl<String>(validators: [Validators.required])
});
void cleanUp() {
print('cleaning up');
}
void onSubmitPressed(BuildContext context) {
// _saveRegistrationLocallyUseCase.invoke(
// form.control(_self.emailKey).value as String ?? '',
// form.control(_self.passwordKey).value as String ?? '');
}
}
abstract class VpViewModelNew {
VpViewModelNew() {
if (onCreate != null) {
onCreate();
print('creating');
}
}
void onCreate() {}
}
When I navigate to the page that has the signInViewModelProvider, it prints to the console:
flutter: signInPage building
flutter: creating
flutter: cleaning up
Then popping the page from the stack with Navigator.pop() prints nothing.
Then navigating to the page again prints the same 3 lines in the same order.
I expected onDispose to be called after Navigator.pop(), and not when navigating to the page that reads the provider. Why is onDispose being called directly after creation, and not when using Navigator.pop() (when I expected the provider to be disposed of since no other views reference it)?
Edit: I access the provider with final viewModel = context.read<SignInViewModel>(signInViewModelProvider);
I don't need to listen since I don't need to rebuild the page on
change. Is consumer less performant for this?
No, the performance is meaningless, even if it's listening it's not really affecting the performance because as a Provider there is no way to notify (which is not the case with a state notifier or change notifier)
Also if you don't care to listen after the value has been read The auto dispose understand no one is watching it and it disposes, it's better to use context.read when using tap or gestures that modify something
(I realize this is late to the party but maybe it'll help somebody)
The Riverpod docs come out pretty strongly against using read for the reason you said, i.e. performance/rebuilding concerns.
Basically you should always use watch except:
If you want your custom callback function called when it updates (use listen)
If the actual reading is happening asynchronously or in response to user action (like in an onPressed): this is the only time to use read.
If you're having issues with your widgets rebuilding too often, Riverpod has some ways to deal with that that don't involve using read.

Flutter + Bloc 6.0.6. BlocBuilder's "builder" callback isn't provided with the same state as the "buildWhen" callback

I'm building a tic-tak-toe app and I decided to learn BLoC for Flutter along. I hava a problem with the BlocBuilder widget.
As I think about it. Every time Cubit/Bloc that the bloc builder widget listens to emits new state the bloc builder goes through this routine:
Call buildWhen callback passing previous state as the previous parameter and the newly emitted state as the current parameter.
If the buildWhen callback returned true then rebuild.
During rebuilding call the builder callback passing given context as context parameter and the newly emitted state as state parameter. This callback returns the widget that we return.
So the conclusion is that the current parameter of the buildWhen call is always equal to the state parameter of the builder call. But in practice it's different:
BlocBuilder<GameCubit, GameState>(
buildWhen: (previous, current) => current is SetSlotSignGameState && (current as SetSlotSignGameState).slotPosition == widget.pos,
builder: (context, state) {
var sign = (state as SetSlotSignGameState).sign;
// Widget creation goes here...
},
);
In the builder callback, it throws:
The following _CastError was thrown building BlocBuilder<GameCubit, GameState>(dirty, state:
_BlocBuilderBaseState<GameCubit, GameState>#dc100):
type 'GameState' is not a subtype of type 'SetSlotSignGameState' in type cast
The relevant error-causing widget was:
BlocBuilder<GameCubit, GameState>
The method where I emit the states that is in the GameCubit class:
// [pos] is the position of the slot clicked
void setSlotSign(Vec2<int> pos) {
// Some code
emit(SetSlotSignGameState(/* Parameter representing the sign that is being placed in the slot*/, pos));
// Some code
emit(TurnChangeGameState());
}
Briefly about types of states. SetSlotSignGameState is emitted when a user taps on a slot in the tic-tac-toe grid and the slot is empty. So this state means that we need to change sign in some slot. TurnChangeGameState is emitted when we need to give the turn to the next player.
Temporary solution. For now I fixed it by saving the state from buildWhen callback in a private field of the widget's state and then using it from the builder. BlocListener also has this problem but there I can just move the check from listenWhen callback into listen callback. The disadvantage of this solution is that it's very inelegant and inconvenient.
buildWhen is bypassed (not even called) on initial state OR when Flutter requests a rebuild.
I have created a small "test" to emphasize that:
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(BlocTestApp());
}
class BlocTestApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider<TestCubit>(
// Create the TestCubit and call test() event right away
create: (context) => TestCubit()..test(),
child: BlocBuilder<TestCubit, String>(
buildWhen: (previous, current) {
print("Call buildWhen(previous: $previous, current: $current)");
return false;
},
builder: (context, state) {
print("Build $state");
return Text(state);
},
),
),
);
}
}
class TestCubit extends Cubit<String> {
TestCubit() : super("Initial State");
void test() {
Future.delayed(Duration(seconds: 2), () {
emit("Test State");
});
}
}
OUTPUT:
I/flutter (13854): Build Initial State
I/flutter (13854): Call buildWhen(previous: Initial State, current: Test State)
As can be seen from output the initial state is built right away without calling buildWhen. Only when the state changes buildWhen is examined.
Other References
This behavior is also outlined here by the creator of Flutter Bloc library (#felangel):
This is expected behavior because there are two reasons for a
BlocBuilder to rebuild:
The bloc state changed
Flutter marked the widget as needing to be rebuilt.
buildWhen will prevent builds triggered by 1 but not by 2. In
this case, when the language changes, the whole widget tree is likely
being rebuilt which is why the BlocBuilder is rebuilt despite
buildWhen.
Possible solution
In your situation, based on the little code you revealed, is better to store the entire Tic-Tac-Toe configuration in the state and use BLOC events to alter it. In this way you do not need that buildWhen condition.
OR make the check inside the builder function if the logic let you do that (this is the most common used solutions with BLOC).
To respond to you question (if not clear so far :D): Sadly, you can not rely on buildWhen to filter the state types sent to builder function.
Could you please check if SetSlotSignGameState extends the abstract class GameState

flutter bloc - wrong state is sent from bloc to widget

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)