flutter bloc - dispatch event to hosting widget without changing the state - flutter

I got this situation:
screen1 (widget) navigates to screen2 (widget) and passes it a function (callback)
screen2 has its own bloc (bloc2)
screen2 add event to bloc2
bloc2 performs a long async operation
when operation completes, bloc2 should notify screen2 (The problem is here)
screen2 should invoke the passed function
screen1 in a réponse should close screen2
How should bloc2 notify screen2 without changing a state.
Changing the state seems a redundant operation in this case, as I don't see a point in build function to run again (and returning a redundant widget)

Assuming you are using a BlocBuilder to build screen2, then you could use the buildWhen property to determine on which conditions to rebuild, maybe never on state-change?
You can get the blocs public properties and methods using the previous and current parameters. Those parameters are of type BlocAState.
BlocBuilder<BlocA, BlocAState>(
buildWhen: (previous, current) {
// return true/false to determine whether or not
// to rebuild the widget with state
},
builder: (context, state) {
// return widget here based on BlocA's state
}
)
Furthermore, this step:
screen2 should invoke the passed function
Should, probably, be done in a BlocListener or BlocConsumer. Maybe that is already how you do it, if not. Look into it..
So to solve your problem I would imagine that you should switch your BlocBuilder to a BlocConsumer, actually change the state, because your "long async operation" does change the state of screen2 from "not done the long thing", to "has done the long thing". Then your BlocListener can invoke the passed function, and the buildWhen property can make sure to not rebuild your screen2.
The BlocConsumer looks like this:
BlocConsumer<BlocA, BlocAState>(
listenWhen: (previous, current) {
// return true/false to determine whether or not
// to invoke listener with state
},
listener: (context, state) {
// do stuff here based on BlocA's state
// I.e. invoke the passed function
},
buildWhen: (previous, current) {
// return true/false to determine whether or not
// to rebuild the widget with state
},
builder: (context, state) {
// return widget here based on BlocA's state
}
)

Related

Flutter Riverpod: using ref.watch in the constructor for a ChangeNotifier, how to avoid Notifier being recreated upon changes

I have a ChangeNotifier, which I use with ChangeNotifierProvider to track the state of a screen in my app. It has a constructor:
CategoryViewState(ProviderRefBase ref, int listId) {
subjects = ref.watch(otherProvider.select((value) => value.getSubjects()));
}
The problem I'm encountering is that when otherProvider.getSubjects() changes, the whole ChangeNotifier is recreated from scratch, rather than the subjects list being updated. This means the state of the page is lost.
Is there a fix or another way to do this that avoids this happening?
Just put the widget that list to the changes in a Consumer widget and watch changes inside it:
Consumer(builder: (context, ref, child) {
final subjects = ref.watch(otherProvider.select((value) => value.getSubjects()));
return YourWidget(); // Just this widget will be rebuilt
},)

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)

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

show alert on cubit state in flutter

I know we can return different widgets on certain state of cubit, but how can we show an alert or other interactions on states:
BlocBuilder<LoginCubit, LoginState> (
builder: (context, LoginState loginState) {
if (loginState is LoginInitial) {
return Text("LoginInitial");
} else if (loginState is LoginLoading) {
return Text("LoginLoading");
} else if (loginState is LoginLoaded) {
return Text("LoginLoaded");
} else if (loginState is LoginError) {
return Text("LoginError");
} else {
return Container();
}
},
)
here in LoginError I want to show an alert dialog.
You can use BlocConsumer, which has both a builder and a listener:
The builder attribute is the widget builder callback you already know
The listener is a callback called when the state changes and you can do pretty much anything there.
For more fine grained control you can use buildWhen and listenWhen, which trigger respectively the builder or listener callbacks if they return true.
For example you can see how I've used BlocConsumer to show a SnackBar when an error state occurs here.
Don't mind the double check on the type
if (state is RegionalReportLoadingError)
because it's probably useless (according to the docs) and I just wanted to be sure about that when I did not have the usage of listenWhen very clear.
You can check more about BlocConsumer in the docs (Unfortunately I cannot link the anchor).
Showing dialogs, snackbars, exiting the screen or navigating somewhere else - these kind of tasks should be done in listeners, like this:
useCubitListener<BookDetailsCubit, BookDetailsPageState>(cubit, (cubit, state, context) {
state.maybeWhen(
saveBook: () => context.router.pop<bool>(true),
deleteBook: () => context.router.pop<bool>(true),
orElse: () => null,
);
}, listenWhen: (state) => (state is BookDetailsPageSaveBook || state is BookDetailsPageDeleteBook));
Here I used cubits with hooks. useCubitListener() is a global function. More on this in my Flutter cubits + hooks tutorial.

Dispatch first bloc event or cubit method, on page start inside StatelessWidget

I have 10 buttons in main menu of my app and each of them contains BlocBuilder inside them.
So when I click on those buttons to open a new page, I want to dispatch the first event, but I don't know how. I can change all classes to stateful widget and then call bloc.dispatch(event) inside initialState() function, but I would like to discover another way, and not sure whether it's the best way
In order to trigger the first event/method call inside BlocBuilder I had to add bloc argument and give parameter provided my BlocProvider, only after this I managed to call my method.
BlocBuilder<MyCubit, MyState>(
bloc: BlocProvider.of<MyCubit>(context)..myFunction(),
builder: (BuildContext context, state) {
//Your code...
}
you can use .. operator to add event while declaring like
BlocProvider(
create: (context) => FirstBloc()..add(InitialiEvent()), // <-- first event,
child: BlocBuilder<FirstBloc, FirstState>(
builder: (BuildContext context, state) {
...
},
),
or you can do it inside the initState method as well