Why when i use a class in dart with equatable and just a list as property the copyWith method return the same object, same hascode - flutter

Im using bloc and it was working as expected but today i notice a strage behaviour when i was sending the same state (RefreshState) using copyWith, the state wasnt trigger after second call. then i did a test creating two objects and compared them but the result was they are the same object, very odd.
So why is this happen?, this is my class:
class Model extends Equatable {
final List<Product> mostBuyProducts;
const Model({
this.mostBuyProducts,
});
Model copyWith({
List<Product> mostBuyProducts,
}) =>
Model(
mostBuyProducts: mostBuyProducts ?? this.mostBuyProducts,
);
#override
List<Object> get props => [
mostBuyProducts,
];
}
and then i use the CopyWith method like (inside the bloc):
Stream<State> _onDeleteProduct(OnDeleteProduct event) async* {
state.model.mostBuyProducts.removeWhere((p) => p.id == event.id);
var newMostBuyProducts = List<Product>.from(state.model.mostBuyProducts);
final model1 = state.model;
final model2 = state.model.copyWith(mostBuyProducts: newMostBuyProducts);
final isEqual = (model1 == model2);
yield RefreshState(
state.model.copyWith(mostBuyProducts: newMostBuyProducts));
}
isEqual return true :/
BTW this is my state class
#immutable
abstract class State extends Equatable {
final Model model;
State(this.model);
#override
List<Object> get props => [model];
}

Yes because lists are mutable. In order to detect a change in the list you need to make a deep copy of the list. Some methods to make a deep copy are available here : https://www.kindacode.com/article/how-to-clone-a-list-or-map-in-dart-and-flutter/
Using one such method in the solution below! Just change the copyWith method with the one below.
Model copyWith({
List<Product> mostBuyProducts,
}) =>
Model(
mostBuyProducts: mostBuyProducts ?? [...this.mostBuyProducts],
);

Related

How do I update a Map with BLoC and equatable?

I am still a beginner with BLoC architecture. So far the UI updates when using int, bool, and other basic data types. But when it comes to Maps it really confuses me. My code basically looks like this:
my state
enum TalentStatus { initial, loading, loaded, error }
class TalentState extends Equatable {
const TalentState({
required this.talentStatus,
this.selectedService = const {},
required this.talents,
this.test = 0,
});
final TalentStatus talentStatus;
final Talents talents;
final Map<String, Service> selectedService;
final int test;
TalentState copyWith({
TalentStatus? talentStatus,
Talents? talents,
Map<String, Service>? selectedService,
int? test,
}) =>
TalentState(
selectedService: selectedService ?? this.selectedService,
talentStatus: talentStatus ?? this.talentStatus,
talents: talents ?? this.talents,
test: test ?? this.test,
);
#override
List<Object> get props => [talentStatus, talents, selectedService, test];
}
my event
abstract class TalentEvent extends Equatable {
const TalentEvent();
#override
List<Object> get props => [];
}
class TalentStarted extends TalentEvent {}
class TalentSelectService extends TalentEvent {
const TalentSelectService(
this.service,
this.talentName,
);
final Service service;
final String talentName;
}
and my bloc
class TalentBloc extends Bloc<TalentEvent, TalentState> {
TalentBloc(this._talentRepository)
: super(TalentState(
talentStatus: TalentStatus.initial, talents: Talents())) {
on<TalentSelectService>(_selectService);
}
final TalentRepository _talentRepository;
Future<void> _selectService(
TalentSelectService event,
Emitter<TalentState> emit,
) async {
state.selectedService[event.talentName] = event.service;
final selectedService = Map<String, Service>.of(state.selectedService);
emit(
state.copyWith(
selectedService: selectedService,
),
);
}
}
whenever an event TalentSelectService is called BlocBuilder doesn't trigger, what's wrong with my code?
Your Service object must be comparable. One suggestion is that it extends Equatable. Either way it have to implement (override) the == operator and hashCode
The reason your BlocBuilder doesn't trigger is (probably) that it doesn't recognize that there has been a change in the Map.

How to replace copyWith so as not to copy, but to create a new state?

I have a bloc which is responsible for switching indexes in the Navogation Bottom Bar.It is implemented in such a way that it copies the old state and changes it. I need to replace copyWith and make it not copy but create a new state. How can this be implemented and rewritten given bloc?
class BottomNavyBloc extends Bloc<BottomNavyEvent, BottomNavyState> {
BottomNavyBloc() : super(const BottomNavyState()) {
on<ChangePageEvent>(
(event, emit) => emit(
state.copyWith(index: event.index),
),
);
}
}
abstract class BottomNavyEvent extends Equatable {
const BottomNavyEvent();
#override
List<Object> get props => [];
}
class ChangePageEvent extends BottomNavyEvent {
final int index;
const ChangePageEvent({
required this.index,
});
#override
List<Object> get props => [index];
}
My state:
class BottomNavyState extends Equatable {
const BottomNavyState({
this.index = 0,
});
final int index;
#override
List<Object> get props => [index];
}
class ChangePageState extends BottomNavyState {
}
We use
emit(state.copyWith(index: event.index))
to say that we are copying all the elements from the previous state by changing index.
Your state BottomNavyState has only one variable as of now. So, the above copyWith acts similar to using emitting new state.
We should not try to change or override the method copyWith because it beats the method's actual purpose.
Instead, you could use
emit(BottomNavyState(index: event.index))
to use a new state constructor instead of copying from previous state.

Riverpod state class default value

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

Why we should use Equatable in Flutter Bloc?

I understand that Equatable helps to compare two instances of object without doing it manually.
But where exactly I can use it in Flutter Bloc?
Here is the example of usage Equatable:
Where it could be useful?
abstract class TodosState extends Equatable {
const TodosState();
#override
List<Object> get props => [];
}
class TodosLoadInProgress extends TodosState {}
class TodosLoadSuccess extends TodosState {
final List<Todo> todos;
const TodosLoadSuccess([this.todos = const []]);
#override
List<Object> get props => [todos];
#override
String toString() => 'TodosLoadSuccess { todos: $todos }';
}
class TodosLoadFailure extends TodosState {}
Object and data comparison is always hard to do when it comes to stream as we need to decide state updation based on it.
we required Equatable as it overrides == and hashCode internally, which saves a lot of boilerplate code. In Bloc, we have to extend Equatable to States and Events classes to use this functionality.
abstract class TodosState extends Equatable {}
So, that means TodosState will not make duplicate calls and will not going to rebuild the widget if the same state occurs.
Let's see props usage in Equatable and what makes it special
Define State without props:
class TodosLoadSuccess extends TodosState {}
Define State with props:
props declared when we want State to be compared against the values which declared inside props List
class TodosLoadSuccess extends TodosState {
final String name;
final List<Todo> todos;
const TodosLoadSuccess([this.name, this.todos = const []]);
#override
List<Object> get props => [name, todos];
}
If we remove the name from the list and keep a list like [this.todos], then State will only consider the todos field, avoiding the name field. That is why we used props for handling State changes.
Bloc Stream Usage:
As we extending State with Equatable that makes a comparison of old state data with new state data. For example, let's look at the below example here TodosState will build a widget only once, which will avoid the second call as it is duplicated.
#override
Stream<TodosState> mapEventToState(MyEvent event) async* {
final List<Todo> todos = [Todo(), Todo()];
yield TodosLoadSuccess(todos);
yield TodosLoadSuccess(todos); // This will be avoided
}
Detail Blog: https://medium.com/flutterworld/flutter-equatable-its-use-inside-bloc-7d14f3b5479b
I think it is useful for comparing what state is in BlocBuilder.
Below code is a good example of using Equatable.
if(state is [Some State])
#override
Widget build(BuildContext context) {
return BlocBuilder<SongsSearchBloc, SongsSearchState>
bloc: BlocProvider.of(context),
builder: (BuildContext context, SongsSearchState state) {
if (state is SearchStateLoading) {
return CircularProgressIndicator();
}
if (state is SearchStateError) {
return Text(state.error);
}
if (state is SearchStateSuccess) {
return state.songs.isEmpty
? Text(S.EMPTY_LIST.tr())
: Expanded(
child: _SongsSearchResults(
songsList: state.songs,
),
);
} else {
return Text(S.ENTER_SONG_TITLE.tr());
}
},
);
}

Inner method in specific state is not recognized, BloC pattern

I build a flutter project by using Bloc State management. But There are something that i don't understand. I know how to make State as Abstract class which it's implement for every it's children.
I have class state lake below, please focus to class MainHomeLoaded
abstract class MainHomeState extends Equatable{
MainHomeState();
}
class MainHomeUnInitialized extends MainHomeState{
#override
List<Object> get props => null;
}
class MainHomeLoading extends MainHomeState{
#override
List<Object> get props => null;
}
class MainHomeLoaded extends MainHomeState{
final List<Article> listArticle;
final bool hasReachedMax;
MainHomeLoaded({#required this.listArticle, this.hasReachedMax});
MainHomeLoaded copyWith({
List<Article> article,
bool hasReacedMax,
}){
return MainHomeLoaded(
listArticle: article ?? this.listArticle,
hasReachedMax: hasReacedMax ?? this.hasReachedMax);
}
#override
List<Object> get props => null;
}
class MainHomeError extends MainHomeState{
final String errorMsg;
MainHomeError({#required this.errorMsg});
#override
List<Object> get props => [errorMsg];
}
then i have MainHomeBloc class with implement Bloc method like mapEventtoState() and inside this method i made conditional like below(again please focus to conditional MainHomeLoaded):
#override
Stream<MainHomeState> mapEventToState(MainHomeEvent event) async*{
if(event is CallHomeLatestNews && !_hasReachedMax(state)){
if(state is MainHomeUnInitialized){
ResponseArticle responseArticle = await mainHomeRepository.latestNews(event.page);
if(responseArticle.status == 'success'){
List<Article> data = responseArticle.data;
yield MainHomeLoaded(listArticle: data);
}else{
yield MainHomeError(errorMsg: responseArticle.message);
}
}
if(state is MainHomeLoaded){
ResponseArticle responseArticle = await mainHomeRepository.latestNews(event.page);
if(responseArticle.status == 'success'){
List<Article> data = responseArticle.data;
yield data.isEmpty ? state.copyWith(hasReacedMax: true)
: MainHomeLoaded(listArticle: state.listArticle + data, hasReachedMax: false);
}
}
}
This is part that i don't understand at all, as you can see we have consider that state are in MainHomeLoaded because inside if conditional, but i got error building and my IDE show red line and also method copyWith()doesn't recognize. The error display like this:
what IDE says is
method copyWith() is not define for the class 'MainHomeState'
Can someone help me to give simple explanation for this case? Thanks
FYI i used Flutter in this version 1.12.13 and Dart version 2.7.0
Finally i found what my main problem. Perhaps it's bit different when we using state in BlocBuilder (in Screens) that automatically known specific state. So what i have to do is just casting it to be child that i wanted. So the solution of this case is like this:
if(state is MainHomeLoaded){
MainHomeLoaded mainHomeLoaded = state as MainHomeLoaded; // what i need
ResponseArticle responseArticle = await mainHomeRepository.latestNews(defaultPage);
if(responseArticle.status == 'success'){
List<Article> newData = responseArticle.data;
if(newData.isEmpty){
mainHomeLoaded.copyWith(hasReacedMax: true);
yield mainHomeLoaded;
}
defaultPage++;
}else{
print('gagal');
yield MainHomeError(errorMsg: responseArticle.message);
}
}
i hope it will help someone in future.