How do I update a Map with BLoC and equatable? - flutter

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.

Related

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.

Best Practice for storing temp data across States in Bloc

I have a simple counter application made by Flutter using Bloc. The idea is after the user presses the increase button, it will delay for 2 seconds, show loading, then increase/decrease the value.
The counter bloc contains 2 states, CounterValue and CounterLoading. However, whenever I increase, the bloc starts creating a new CounterLoading object with a value of 0. To deal with this, I have to pass the current value at the CounterValue state to CounterLoading state, and after 2 seconds, I have to pass again the current value of Loading state to CounterValue state to increase the value. Hence this seems to be pretty redundant and confused when it comes to real situations where we have multiple states in the middle which don't need data while the first and last state emitted are dependent.
What is the best practice to store temp data across states using bloc?
counter_state.dart
class CounterState {
int value = 0;
}
class CounterLoading extends CounterState {}
class CounterValue extends CounterState {}
counter_bloc.dart
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterValue()) {
on<IncrementCounter>(
(event, emit) async {
emit(CounterLoading()..value = state.value);
await Future.delayed(const Duration(seconds: 2));
emit(CounterValue()..value = state.value + 1);
},
);
}
I would recommend investigating immutable states, which is very convenient while using BLoC. It means that instead of directly changing property values in the state, you are rather creating a new state object and replacing the previous state.
For this specific problem, I would recommend you to have a more complex state instead of having two separate states for Loading/Value. For instance:
State:
class CounterState {
const CounterState({
this.value = 0,
this.isLoading = false
});
final int value;
final bool isLoading;
}
BLoC:
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterValue()) {
on<IncrementCounter>(
(event, emit) async {
final cur = state.value;
emit(CounterState(value: cur, isLoading: true));
await Future.delayed(const Duration(seconds: 2));
emit(CounterState(value: cur + 1, isLoading: false));
},
);
}
When state is more complicated than just 2 values, you should combine the use of Equatable package and copyWith method in your state.
Equatable makes it easy to tell that stateA != StateB. It's a replacement for manually overriding the == operator in your state class.
copyWith just returns a new state with zero or more of its values changed. It should be used basically every time you want to emit a new state.
State:
class CounterState extends Equatable {
const CounterState({
this.value = 0,
this.isLoading = false
});
final int value;
final bool isLoading;
#override
List<Object> get props => [value, isLoading];
CounterState copyWith({
int? value,
bool? isLoading,
}) {
return CounterState(
value: value ?? this.value,
isLoading: isLoading ?? this.isLoading,
);
}
}
Cubit:
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(const CounterState());
Future<void> increment() async {
emit(state.copyWith(isLoading: true));
await Future<void>.delayed(const Duration(seconds: 2));
emit(state.copyWith(value: state.value + 1, isLoading: false));
}
}
And for even larger apps where you might use custom objects, not just integers, strings and booleans, I recommend you take a look at freezed for creating models to include in your state with their own copyWith methods, etc. automatically generated.
Emits would look something like this for an AuthState with a User model:
void updateUserName(String newName) {
emit(state.copyWith(user: state.user.copyWith(name: newName)));
}

Flutter Riverpod: Filter rebuilds with StateNotifier and .select()

This is my current state management solution
class UserState {
final int id;
final String name;
}
class UserNotifier extends StateNotifier<UserState> {
UserNotifier() : super(User(1, 'Pero Peric'));
}
final userNotifierProvider = StateNotifierProvider((ref) => UserNotifier());
I want to rebuild my UI only when the name changes not the id!
Riverpod provides a way to do this link but I can't get it working with my StateNotifier.
I would write it like this but it isn't working like this.
// inside Consumer widget
final userName = watch(userNotifierProvider.select((value) => value.state.name));
Can you refactor my code to work or propose another solution?
Any help is appreciated!
According to the doc, "this method of listening to an object is currently only supported by useProvider from hooks_riverpod and ProviderContainer.listen".
Try to create another provider, which you can use in UI.
final nameProvider = StateProvider<String>((ref) => ref.watch(userNotifierProvider.select((user) => user.name)));
class UserState {
UserState({
required this.id,
required this.name,
});
final int id;
final String name;
}
class UserNotifier extends StateNotifier<UserState> {
UserNotifier() : super(UserState(id: 1, name: 'Pero Peric'));
}
final userNotifierProvider =
StateNotifierProvider<UserNotifier, UserState>((ref) => UserNotifier());
In consumer,
final userName =
watch(userNotifierProvider.select((value) => value.name)); // value is the state

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

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],
);

How can I use variable of CubitState inside Cubit? Flutter/Bloc

so I don't have any idea how to take argument from mine Cubit state which is AnswerPicked in this case, there is a code from states file.
part of 'answer_cubit.dart';
abstract class AnswerState extends Equatable {
const AnswerState();
#override
List<Object> get props => [];
}
class AnswerInitial extends AnswerState {}
class AnswerPicked extends AnswerState {
final String answer;
AnswerPicked({
this.answer,
});
String toString() => '{AnswerPicked: $answer}';
}
I want to use it in Cubit function right there:
part 'answer_state.dart';
class AnswerCubit extends Cubit<AnswerState> {
final ExamScoreCubit scoreCubit;
AnswerCubit({
#required this.scoreCubit,
}) : super(AnswerInitial());
List<String> userAnswersList = [];
void pickAnswer(String answer) {
emit(AnswerInitial());
emit(AnswerPicked(answer: answer));
}
void takeAnswer(String questionAnswer, int type) {
if(state is AnswerPicked){
userAnswersList.add(state.answer); // state.answer don't work
scoreCubit.checkAnswer(AnswerPicked().answer, questionAnswer, type); // AnswerPicked().answer don't work
}
emit(AnswerInitial());
}
}
In void takeAnswer() I don't want to pass it throw argument inside the widget tree using context. Any ideas how to do it?
userAnswersList.add((state as AnswerPicked) .answer);