I yield same state but with different object in my bloc but BlocBuilder not called again.
How I can do this scenario ?
My mapEventToState is
if (event is EditUserProfileImageChanged) {
UserProfile newUserProfile = state.userProfile;
newUserProfile.avatar = event.imgSrc;
yield EditUserProfileTotalState(userProfile: newUserProfile);
}
When we yield a state in the private mapEventToState handlers, we are always yielding a new state instead of mutating the state. This is because every time we yield, bloc will compare the state to the nextState and will only trigger a state change (transition) if the two states are not equal. If we just mutate and yield the same instance of state, then state == nextState would evaluate to true and no state change would occur.
If you want to change the value of the state, make a copyWith function for your model class.
class UserProfile extends Equatable {
final String name;
final Image avatar;
const UserProfile({this.name, this.avatar});
UserProfile copyWith({String name, Image avatar,}) {
return UserProfile(
name: name ?? this.name,
avatar: avatar?? this.avatar,
);
}
#override
List<Object> get props => [name, avatar];
}
if (event is EditUserProfileImageChanged) {
var newState = state.userProfile.copyWith(avatar: event.imgSrc);
yield EditUserProfileTotalState(userProfile: newState);
}
Removing Equatable solve the problem.
Removing equatbale rebuilds every time even values of the properties are not changed. Instead create new state instance every time.
Related
I'm using flutter_bloc. I have one class to hold state.
class FondoFormState extends Equatable {
const FondoFormState({
this.transferAccountDetails,
});
FondoFormState.copy(
FondoFormState copy, {
List<TransferAccountDetails>? transferAccountDetails,
}) : transferAccountDetails = transferAccountDetails ?? copy.transferAccountDetails;
final List<TransferAccountDetails>? transferAccountDetails;
#override
List<Object?> get props => <Object?>[
transferAccountDetails,
];
#override
bool get stringify => true;
}
I'm calling the event from the UI..
with context.read<FondoFormBloc>().add(AddAccountEvent(accounts));
My bloc is is receiving the event..(from the print statement)
However when I print the state I get null for transferAccountDetails
on< AddAccountEvent >((AddAccountEvent event, Emitter<FondoFormState> emit) {
print(event.transferAccountDetails);
FondoFormState.copy(state, transferAccountDetails: event.transferAccountDetails);
print(state);
// FondoFormState.copy(state, fondoAccounts: fondoAccounts);
});
The issue is probably with the copy method in the state class. It's not holding the state. Wonder can anyone notice my error!
Calling emit on the state updates the state
I am making first steps with Riverpod and just want to check if my understanding of handling changes of some data class properties using Riverpod is correct.
Imagine, I have a data class like that:
class MyClass {
final String name;
final int id;
const MyClass(this.name, this.id);
}
Then I create a StateNotifier:
class MyClassStateNotifier extends StateNotifier<MyClass> {
MyClassStateNotifier(MyClass state) : super(state);
void setName(String name) {
state.name = name;
}
}
And this won't work - UI will not be rebuilt after calling setName this way.
So I need to modify classes in the following way:
class MyClass {
final String name;
final int id;
const MyClass(this.name, this.id);
MyClass copyWith({name, id}) {
return MyClass(name ?? this.name, id ?? this.id);
}
}
and the StateNotifier as following:
class MyClassStateNotifier extends StateNotifier<MyClass> {
MyClassStateNotifier(MyClass state) : super(state);
void setName(String name) {
state = state.copyWith(name: name);
}
}
This pair will work and the UI will be rebuilt.
So, my question: does one always need to reinstantiate the object in this way?..
From my perspective, this is a bit strange (simple datatypes like String / int do not require this) and the boilerplate for copyWith method might become pretty huge if I have a dozen of object's properties.
Is there any better solution available for Riverpod or is it the only one and correct?..
Thanks in advance! :)
To trigger a state change you have to use the state setter. The implementation looks like this:
#protected
set state(T value) {
assert(_debugIsMounted(), '');
final previousState = _state;
_state = value;
/// only notify listeners when should
if (!updateShouldNotify(previousState, value)) {
return;
}
_controller?.add(value);
// ...
The internal StreamController<T> _controller needs to be triggered (add), to notify listeners (in this case riverpod) about updates.
By using state.name = something you're not informing the StateNotifier about a new state (not calling the state setter). Only your object holds the new value but nobody was notified.
Your state is mutable and that very often leads to such misbehavior. By using an immutable object you can prevent such errors in the first place. Write it yourself or use freezed.
Learn more about immutability in my talk
e.g. I have class ProfileModel with bunch of fields
many of them don't have default values unless they're initialising when I get user info from backend
with riverpod I need to write something like
final profileProvider = StateNotifierProvider((ref) => ProfileState());
class ProfileState extends StateNotifier<ProfileModel> {
ProfileState() : super(null);
}
I understand I need to pass something like ProfileState.empty() into super() method instead passing null
but in this case I have to invent default values for every ProfileModels fields
this sounds weird for me, I don't want to break my head to care about empty or default state of EVERY model in project
in my example there are no default values for user name, age etc
this is pure immutable class
what I'm doing wrong or missing?
or I can declare model as nullable extends StateNotifier<ProfileModel?>
but I'm not sure is this a good way
It is fine to use the StateNotifier with a nullable model. If you semantically want to indicate the value can be actually absent, I would say that that having null is alright.
However, what I usually do and what I think is better, is create a state model that contains the model, but also properties that relate to the different states the app could be in.
For example, while fetching the data for the model from an API, you might want to have a loading state to show a spinner in the UI while waiting for the data to be fetched. I wrote an article about the architecture that I apply using Riverpod.
A simple example of the state model would be:
class ProfileState {
final ProfileModel? profileData;
final bool isLoading;
ProfileState({
this.profileData,
this.isLoading = false,
});
factory ProfileState.loading() => ProfileState(isLoading: true);
ProfileState copyWith({
ProfileModel? profileData,
bool? isLoading,
}) {
return ProfileState(
profileData: profileData ?? this.profileData,
isLoading: isLoading ?? this.isLoading,
);
}
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ProfileState &&
other.profileData == profileData &&
other.isLoading == isLoading;
}
#override
int get hashCode => profileData.hashCode ^ isLoading.hashCode;
}
I'm using flutter_bloc library to manage the state of the widgets. I'm trying to update a variable in the state that is a List. When I capture the state and modify the desired value, then yield the new state, the state doesn't update, but the clone of the state is changed.
Stream<ItemState> _updateRestrictions(ItemUpdateRestrictionValue event) async * {
if (state is ItemLoaded) {
final restrictions = (state as ItemLoaded).restrictions;
final newState = restrictions.map((restriction) {
if (restriction.id == event.restrictionId) {
return ItemRestriction(
id: restriction.id,
name: restriction.name,
restrictionType: restriction.restrictionType,
restrictionValues: restriction.restrictionValues,
restrictionValue: event.restrictionValueId
);
}
return restriction;
}).toList();
yield ItemLoaded(restrictions: newState);
}
}
Am I doing something wrong? Or how do you update the state using flutter_bloc correctly?
That probably happens because you use Equatable on your ItemState class and yield the same state back-to-back which is IteamLoaded().
You may wanna read this https://bloclibrary.dev/#/faqs?id=when-to-use-equatable
In the login view, if the user taps on the login button without having inserted his credentials, the LoginFailState is yield and the view reacts to it. If he taps again, this LoginFailstate is yield again, but the view doesn't react to it. So, is there a way to yield more times the same state?
There is some code to better explain my situation:
class LoginBloc extends Bloc<LoginEvent, LoginState> {
#override
LoginState get initialState => LoginUninitialized();
#override
Stream<LoginState> mapEventToState(LoginEvent event) {
if (event is loginButtonPressed) {
yield LoginFailState();
}
}
View:
#override
Widget build(BuildContext context) {
return BlocBuilder(
bloc: _loginBloc,
builder: (BuildContext context, LoginState state) {
if (state is LoginFail) {
print ('Login fail');
}
return Column(
...
)
You can receive an update for the "same" State if you don't extend Equitable, or implement your own '==' logic which makes the two LoginFailStates equal.
The solution is to yield a different State in between, like in the Bloc example.
yield LoginLoading();
It gets called on every login button tap. Felangel's LoginBloc example.
By default BLoC pattern will not emit state when the same state will be passed one after another. One way to do this is to pass your initial BLoC state after passing LoginFailState.
So after user clicks on the button with wrong credentials passed states will not be:
LoginFailState()
LoginFailState()
but
LoginFailState()
LoginEmptyState()
LoginFailState()
LoginEmptyState()
Which will make UI react to each of them.
But I think that the best and cleanest solution is to pass LoadingState from BLoC before passing LoginFailState().
You can follow the blog post that I have recently written regarding this topic.
Problem
When you try to emit a new state that compares equal to the current state of a Bloc, the new state won't be emitted.
This behavior is by design and is discussed here.
When I say "compares equal" I mean that the == operator for the two state objects returns true.
Solution
There are two proper approaches:
Your state class should NOT extend Equatable. Without Equatable, two objects of the same class with the same fields will NOT compare as equal, and this new state will always be emitted.
Sometimes you need your state class to extend Equatable. In this case, just add the result of the identityHashCode(this) function call to your props getter implementation:
class NeverEqualState extends Equatable {
#override
List<Object?> get props => [identityHashCode(this)];
}
Note that I use identityHashCode that works regardless the operator == is overloaded or not. In contrast, hashCode will not work here.
Warning:
Do not use random values in the getter implementation List<Object> get props => [Random().nextDouble()];. Random variables are random, meaning that with extremely low probability you still might get two equal values in a sequence that will break this workaround. This is extremely unlikely, so it's not possible to reproduce and debug this.
You can and should include fields in your get props implementation, but keep in mind that when all fields compare as equal the objects will also compare as equal.
Emitting some other state in-between two equal states works but it forces your BlocBuilder to rebuild part of UI and BlocListener to execute some logic. It's just inefficient.
Finally, why would you like to have a state class extend Equatable but still not compare equal? This might be needed when your state class is actually the root of a hierarchy, where some descendants need to implement the == operator properly, and some need to never compare equal. Here is the example:
class BaseMapState extends Equatable {
const BaseMapState();
#override
List<Object?> get props => [];
}
class MapState extends BaseMapState {
final Map<String, Report> reports;
final Report? selectedReport;
final LatLng? selectedPosition;
final bool isLoadingNewReports;
const MapState(
{this.reports = const {},
this.selectedReport,
this.selectedPosition,
this.isLoadingNewReports = false});
#override
List<Object?> get props => [
...reports.values,
selectedReport,
selectedPosition,
isLoadingNewReports
];
}
class ErrorMapState extends BaseMapState {
final String? error;
const ErrorMapState(this.error);
#override
List<Object?> get props => [identityHashCode(this), error];
}
class NeedsAuthMapState extends ErrorMapState {
const NeedsAuthMapState() : super('Authentication required');
}
class NoInternetMapState extends ErrorMapState {
const NoInternetMapState() : super("No Internet connection");
}
If you use Equitable and tries to emit two equal instances of the same State with different properties, make sure that you override props array. By overriding props array, Equitable will know how to compare state instances.
class TablesLoadedState extends Equatable {
final List<TableEntity> tablesList;
TablesLoadedState(this.tablesList);
#override
List<Object> get props => [tablesList];
}
So, when bloc emits two instances of the same state with different values, these state-instances will be passed to BlocListener and UI will be updated according to new data.
A late possible workaround would be adding a random double to the state get props, this way the state won't be equal and you can yield them one after the other if you want.
also, Random().nextDouble() complexity is O(1) so you don't need to worry about performance
class LoginFailState extends LoginState {
#override
List<Object> get props => [Random().nextDouble()];
}