List of checkbox not updating correctly on the UI (Flutter BloC)

88 Views Asked by At

I'm using the BloC pattern for the first time in my App. And I am having multiple of problems with it.

For my Question i have a list of boolean ( Checkboxes ) The problem is when I click on the boxes sometimes they updated ( the correct behavior ) sometimes they does not updated ( wrong behavior )

The check box does not change until I click multiple times

Cubit Class

class FilterCubit extends Cubit<FilterState> {
  FilterCubit() : super(const FilterInitial());

  int sortByGV = 0;

  void onSortByClick(int index) {
    sortByGV = index;
    emit(OnSortByState(index));
  }

  List<bool> choiceSportsList = [];

  void fillChoiceSportsList(int length) {
    for (int i = 0; i < length; i++) {
      choiceSportsList.add(false);
    }
  }

  void onChoiceSportClick(int index) {
    if (choiceSportsList[index]) {
      choiceSportsList[index] = false;
      emit(const OnChoiceSportState(false));
    } else {
      choiceSportsList[index] = true;
      emit(const OnChoiceSportState(true));
    }
  }
}

State Class

sealed class FilterState extends Equatable {
  const FilterState();

  @override
  List<Object?> get props => [];
}

final class FilterInitial extends FilterState {
  const FilterInitial();

  @override
  List<Object?> get props => [];
}

final class OnSortByState extends FilterState {
  final int index;
  const OnSortByState(this.index);

  @override
  List<Object?> get props => [index];
}

class OnChoiceSportState extends FilterState {
  final bool val;
  const OnChoiceSportState(this.val);

  @override
  List<Object?> get props => [val];
}

UI

showFilterBottomSheetComponent({double? heightFactor}) {
  sl<FilterCubit>().fillChoiceSportsList(10);

  return showModalBottomSheet<void>(
    context: sl<NavigationService>().navigatorKey.currentContext!,
    isScrollControlled: true,
    builder: (BuildContext context) {
      List<String> sports = [
        'Padel',
        'Soccer',
        'Basketball',
        'Tennis',
        'Padbol',
        'Pickleball',
        'Volleyball',
        'Badminton',
        'Squash',
        'Cricket'
      ];

      return FractionallySizedBox(
        heightFactor: heightFactor ?? 0.85,
        child: BlocProvider(
          create: (context) => sl<FilterCubit>(),
          child: SafeArea(
            child: Padding(
              padding:
                  EdgeInsets.all(AppPadding.p24).copyWith(top: AppPadding.p16),
              child: FilterChooseSportBody(data: sports),
            ),
          ),
        ),
      );
    },
  );
}

class FilterChooseSportBody extends StatelessWidget {
  const FilterChooseSportBody({
    super.key,
    required this.data,
  });

  final List<String> data;

  @override
  Widget build(BuildContext context) {
    final filterC = BlocProvider.of<FilterCubit>(context, listen: true);

    return Column(
      children: [
        for (int i = 0; i < data.length; i++)
          CheckBoxListTileComponent(
            title: data[i],
            value: filterC.choiceSportsList[i],
            onChanged: (value) {
              filterC.onChoiceSportClick(i);
            },
            smallHeight: true,
            style: getBodyMedium(),
          ),
      ],
    );
  }
}

In provider I have notifyListeners();

Do I need in bloc to create a state and emit it for each time??

Thanks.

2

There are 2 best solutions below

0
On BEST ANSWER

I solved the problem with the following code.

First i removed the state class and replace it with List

Then the Cubit class like the following:

List<String> sports = [
  'Padel',
  'Soccer',
  'Basketball',
  'Tennis',
  'Padbol',
  'Pickleball',
  'Volleyball',
  'Badminton',
  'Squash',
  'Cricket',
];

class FilterCubit extends Cubit<List<FilterModel>> {
  FilterCubit() : super([]);

  void fillSport() {
    for (var e in sports) {
      final data = FilterModel(name: e, isActive: false);
      state.add(data);
      emit(state);
    }
  }

  void onChanged(Object? value, int index) {
    state[index].isActive = !state[index].isActive;

    emit([...state]);
  }
}

the UI widget

class FilterChooseSportBody extends StatefulWidget {
  const FilterChooseSportBody({super.key});

  @override
  State<FilterChooseSportBody> createState() => _FilterChooseSportBodyState();
}

class _FilterChooseSportBodyState extends State<FilterChooseSportBody> {
  @override
  void initState() {
    if (sl<FilterCubit>().state.isEmpty) {
      sl<FilterCubit>().fillSport();
    }

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<FilterCubit, List<FilterModel>>(
      builder: (context, state) {
        return Column(
          children: [
            for (int i = 0; i < state.length; i++)
              CheckBoxListTileComponent(
                title: sl<FilterCubit>().state[i].name,
                value: state[i].isActive,
                onChanged: (value) {
                  sl<FilterCubit>().onChanged(value, i);
                },
                smallHeight: true,
                style: getBodyMedium(),
              ),
          ],
        );
      },
    );
  }
}

and in the above widget tree i added BlocProvider.value not BlocProvider

BlocProvider.value(value: cubit-here, child: .. )

1
On

You seem to have missed that you need a BlocBuilder, so your build can react to changes emited from your BLoC (or Cubit).

You can look at the first tutorial to check out how to use BLoCs and Cubit.

Your choice of Cubit and what goes into the cubit and what goes outside looks a little weird to me, so I will not try to fit a solution into this.

From what I know about the pattern, you have a BLoC, not a cubit, since you have structured data, not just one simple value like an int. Your data consists of a full list of data, a filtered list of data and the selected data (if any). Those three things should be held in your BLoC, not your UI, and those should be in your emitted states.