Search code examples
flutterdartbloccubit

Missing Statechange in BlocListener (Cubit, initial Load)


I'm refactoring some of my blocs down to cubits and I'm struggling with an unexpected behavior.

Widget

@override
Widget build(BuildContext context) {
  return BlocProvider(
    create: (context) => injector.get<DocumentListCubit>()..load(),
    child: BlocConsumer<DocumentListCubit, DocumentListState2>(
      listener: (context, state) {
        context.read<GlobalBusyIndicatorCubit>().busy = state.phase == DocumentListState2Enum.loading;
      },
      builder: (context, state) {
        switch (state.phase) {
          case DocumentListState2Enum.data:
            return const DocumentListWidget();
          case DocumentListState2Enum.failure:
            return DvtErrorWidget(error: state.error);
          default:
            return const SizedBox();
       }
      },
    ),
  );
}

Cubit:

Future<void> load() async {
   try {
     //await Future.delayed(const Duration(milliseconds: 100)); <-- this line 'solves' the problem
     emit(state.copyWith(state: DocumentListState2Enum.loading));
     final lst = await schreibenRepo.find();
     emit(state.copyWith(state: DocumentListState2Enum.data, documents: lst));
   } catch (e) {
     emit(state.copyWith(state: DocumentListState2Enum.failure, error: e));
     rethrow;
   }
   return Future.delayed(Duration.zero);
}

The Listener does not get the first emitted state 'loading'.

I've debugged equality -> first emitted state 'loading' is fine (not equal previous state) I found out, that it "works" when I give a delay of e.g. 100ms before emitting the state 'loading'. So it seems to me, that this is kind of race condition problem.

I would expect the "create callback" would be called after the BlocConsumer is fully initialized and every child (listener & builder) would receive state change events from the beginning.

What am I understanding or doing wrong here?


Solution

  • As mentioned by manhtuan21 the root cause is a race condition issue: load() method is executed until the first await and during this await the remaining widget tree is built. Therefore your BlocConsumer is not listening yet when the first emit is executed.

    Some ideas how to fix it:

    create: (context) {
        final cubit = DocumentListCubit();
        WidgetsBinding.instance.addPostFrameCallback((_) {
          cubit.load();
        });
        return cubit;
      },
    

    This would delay the execution of the load() method until the whole widget tree is built and the BlocConsumer is listening.

    Quick, easy and feels little bit hacky.


    If you keep your code as it is, the listener will never be called for the first emit(state.copyWith(state: DocumentListState2Enum.loading)); Hence you could set your context.read().busy to true by default. It will be successfully set to false once the new state is emitted.

    Quick and easy.


    Replace the BlocConsumer with a BlocBuilder only and handle the LoadingState in the build method (e.g. return a ProgressIndicator). But this would be a completely different approach than using GlobalBusyIndicatorCubit.

    Bigger refactoring.


    Getting inspired by Marcin Wojnarowski's talk at FlutterCon Europe 24 "Presentation events - missing piece in BLoC" you could introduce a secondary "presentationStream" in your BLoC. Then your progress indicator concept could listen to this stream instead of using a BlocConsumer.

    Not so sure if this is a good design...

    Happy coding.