Search code examples
flutterstate-managementflutter-bloc

Flutter Bloc not updating state correctly when adding items to Hive Box


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


Solution

  • 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