I'm working on a Flutter application where I'm using Bloc for state management and Hive for local storage. I'm facing an issue where the state is not updating correctly when I add an item to the cart. Specifically, when I add an item to the Hive Box, the Bloc does not seem to recognize the state change immediately.
It does recognize the change when I change page or hot-restart the app.
I did add List.from
to try and create a new list to force a state change, since the way I understand is that Bloc won't emit a new state if it's identical to the last one.
If it helps, it doesn't seem to be emitting the CartLoadingState event as well.
Here is the code for my CartBloc:
class CartBloc extends Bloc<CartEvent, CartState> {
final Box cartBox;
CartBloc(this.cartBox) : super(CartInitialState()) {
on<AddItemEvent>((event, emit) async {
emit(CartLoadingState());
print('AddItemEvent started');
final existingItem = cartBox.values.firstWhere(
(item) => (item as Map)['id_curso'] == event.courseItem.id,
orElse: () => null,
);
if (existingItem == null) {
final itemToAdd = event.courseItem.toMap()
..['tipo_certificado'] = event.certificateType == 'digital' ? '65' : '64';
await cartBox.add(itemToAdd);
print('Item added to cartBox');
} else {
print('Item already exists in cartBox');
}
final updatedCartItems = _getCartItems();
emit(CartSuccessState(List.from(updatedCartItems), _calculateTotalPrice(updatedCartItems)));
print('CartSuccessState emitted');
});
on<RemoveItemEvent>((event, emit) async {
emit(CartLoadingState());
print('RemoveItemEvent started');
final key = cartBox.keys.firstWhere(
(k) => (cartBox.get(k) as Map)['id_curso'] == event.id,
orElse: () => null,
);
if (key != null) {
await cartBox.delete(key);
print('Item removed from cartBox');
} else {
print('Item not found in cartBox');
}
final updatedCartItems = _getCartItems();
emit(CartSuccessState(List.from(updatedCartItems), _calculateTotalPrice(updatedCartItems)));
print('CartSuccessState emitted');
});
on<ClearCartEvent>((event, emit) async {
emit(CartLoadingState());
print('ClearCartEvent started');
await cartBox.clear();
print('All items removed from cartBox');
final updatedCartItems = _getCartItems();
emit(CartSuccessState(List.from(updatedCartItems), _calculateTotalPrice(updatedCartItems)));
print('CartSuccessState emitted');
});
on<GetCartItemsEvent>((event, emit) {
emit(CartLoadingState());
print('GetCartItemsEvent started');
final updatedCartItems = _getCartItems();
emit(CartSuccessState(List.from(updatedCartItems), _calculateTotalPrice(updatedCartItems)));
print('CartSuccessState emitted');
});
}
List<UserCourseModel> _getCartItems() {
return cartBox.values.map((item) {
final map = Map<String, dynamic>.from(item as Map);
return UserCourseModel.fromMap(map);
}).toList();
}
double _calculateTotalPrice(List<UserCourseModel> cartItems) {
return cartItems.fold(0.0, (sum, item) => sum + double.parse(item.certificatePrice.replaceAll(',', '.')));
}
}
And here is a snippet of the UI code that triggers the AddItemEvent:
onTap: () {
cartBloc.add(AddItemEvent(course, 'printed'));
Navigator.of(context).pop();
},
Code for CartState:
abstract class CartState extends Equatable {}
class CartInitialState implements CartState {
@override
List<Object?> get props => [];
@override
bool get stringify => false;
}
class CartLoadingState implements CartState {
@override
List<Object?> get props => [];
@override
bool get stringify => false;
}
class CartSuccessState implements CartState {
final List<UserCourseModel> cartItems;
final double totalPrice;
CartSuccessState(this.cartItems, this.totalPrice);
@override
List<Object?> get props => [cartItems, totalPrice];
@override
bool get stringify => false;
}
class CartErrorState implements CartState {
final String message;
const CartErrorState(this.message);
@override
List<Object?> get props => [message];
@override
bool get stringify => false;
}
And CartEvent:
abstract class CartEvent {}
class AddItemEvent extends CartEvent {
final UserCourseModel courseItem;
final String certificateType;
AddItemEvent(this.courseItem, this.certificateType);
}
class RemoveItemEvent extends CartEvent {
final String id;
RemoveItemEvent(this.id);
}
class ClearCartEvent extends CartEvent {}
class GetCartItemsEvent extends CartEvent {}
What might be causing the state not to update immediately in the UI?
UserCourseModel class:
class UserCourseModel {
final String toId;
final String id;
final String userId;
final String status;
final String name;
final String imgUrl;
final String certificatePrice;
final String digitalCertificatePrice;
final String initDate;
final String? conclusionDate;
final String? certificateType;
final List<ModuleModel> modules;
UserCourseModel({
required this.toId,
required this.id,
required this.userId,
required this.status,
required this.name,
required this.imgUrl,
required this.certificatePrice,
required this.digitalCertificatePrice,
required this.initDate,
this.conclusionDate,
this.certificateType,
required this.modules,
});
factory UserCourseModel.fromMap(Map<String, dynamic> map) {
return UserCourseModel(
toId: map['id_to'] as String,
id: map['id_curso'] as String,
userId: map['id_usuario'] as String,
status: map['status_curso'] as String,
name: map['nome_curso'] as String,
imgUrl: map['img_url'] as String,
certificatePrice: map['preco_certificado'] as String,
digitalCertificatePrice: map['preco_certificado_digital'] as String,
initDate: map['data_inicio_curso'] as String,
conclusionDate: map['data_conclusao_curso'] as String? ?? '',
certificateType: map['tipo_certificado'] as String? ?? '',
modules: (map['curso']['lista_modulos'] as List)
.map((item) => ModuleModel.fromMap(Map<String, dynamic>.from(item)))
.toList(),
);
}
Map<String, dynamic> toMap() {
return {
'id_to': toId,
'id_curso': id,
'id_usuario': userId,
'status_curso': status,
'nome_curso': name,
'img_url': imgUrl,
'preco_certificado': certificatePrice,
'preco_certificado_digital': digitalCertificatePrice,
'data_inicio_curso': initDate,
'data_conclusao_curso': conclusionDate ?? '',
'tipo_certificado': certificateType ?? '',
'curso': {
'lista_modulos': modules.map((module) => module.toMap()).toList(),
},
};
}
}
class ModuleModel {
final String id;
final String name;
final String totalTopics;
final List<TopicModel> topics;
ModuleModel({
required this.id,
required this.name,
required this.totalTopics,
required this.topics,
});
factory ModuleModel.fromMap(Map<String, dynamic> map) {
return ModuleModel(
id: map['id_modulo'] as String,
name: map['nome_modulo'] as String,
totalTopics: map['total_topicos'] as String,
topics: (map['lista_topicos'] as List)
.map((item) => TopicModel.fromMap(Map<String, dynamic>.from(item)))
.toList(),
);
}
Map<String, dynamic> toMap() {
return {
'id_modulo': id,
'nome_modulo': name,
'total_topicos': totalTopics,
'lista_topicos': topics.map((topic) => topic.toMap()).toList(),
};
}
}
class TopicModel {
final String orderPosition;
final String id;
final String moduleId;
final String title;
TopicModel({
required this.orderPosition,
required this.id,
required this.moduleId,
required this.title,
});
factory TopicModel.fromMap(Map<String, dynamic> map) {
return TopicModel(
orderPosition: map['ordem_posicao'] as String,
id: map['id_topico'] as String,
moduleId: map['id_modulo'] as String,
title: map['titulo_topico'] as String,
);
}
Map<String, dynamic> toMap() {
return {
'ordem_posicao': orderPosition,
'id_topico': id,
'id_modulo': moduleId,
'titulo_topico': title,
};
}
}
As you said: 'Bloc won't emit a new state if it's identical to the last one.'
And it is a big probability that this is the reason. Your models UserCourseModel
, TopicModel
, ModuleModel
are identical in emitted states.To avoid this you should override equals and hashcode for each of them. A common practice to avoid doing this manually for each model is to use packages like equatable, freezed etc