Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter BLoC mapEventToState gets called only the first time for an event and not called each next time that event is fired

Tags:

flutter

bloc

I have Courses and Tasks. Each Course has many Tasks. That is why I am using different screens in the app to show a list of courses and after a tap on a course, I am navigating to the next screen - a list of tasks. Here is my onTap method of the list of courses:

                          onTap: () {
                            TasksPageLoadedEvent pageLoadedEvent =
                                TasksPageLoadedEvent(
                              courseId: state.courses[index].id,
                              truckNumber: this.truckNumber,
                            );
                            serviceLocator<TaskBloc>().add(pageLoadedEvent);
                            Routes.sailor(
                              Routes.taskScreen,
                              params: {
                                Routes.courseNumber:
                                    state.courses[index].courseNumber,
                                Routes.truckNumber: this.truckNumber,
                                Routes.courseId: state.courses[index].id,
                              },
                            );
                          }

I create a TasksPageLoadedEvent, pass it to the TaskBloc and navigate to the Tasks page.

Here is the TaskBloc and how it handles the mapping Event - State:

@override
  Stream<TaskState> mapEventToState(
    TaskEvent event,
  ) async* {
    if (event is TasksLoadingEvent) {
      yield TasksLoadingState();
    } else if (event is TasksReloadingErrorEvent) {
      yield TasksErrorState();
    } else if (event is TasksFetchedFailureEvent) {
      yield TaskFetchedStateFailureState(error: event.failure);
    } else if (event is TasksPulledFromServerEvent) {
      yield TasksPulledFromServerState(
        truckNumber: event.truckNumber,
        courseNumber: event.courseNumber,
        courseId: event.courseId,
      );
    } else if (event is TasksPageLoadedEvent) {
      yield TasksLoadingState();

      final networkInfoEither = await this.getNetworkInfoQuery(NoQueryParams());

      yield* networkInfoEither.fold((failure) async* {
        yield TasksErrorState();
      }, (success) async* {
        if (success) {
          final getTasksEither = await getTasksQuery(
            GetTasksParams(
              truckNumber: event.truckNumber,
              courseId: event.courseId,
            ),
          );

          yield* getTasksEither.fold((failure) async* {
            yield TaskFetchedStateFailureState(error: "coursesDatabaseError");
          }, (result) async* {
            if (result != null) {
              yield TasksFetchedState(tasks: result);
            } else {
              yield TaskFetchedStateFailureState(
                  error: "coursesFetchFromDatabaseError");
            }
          });
        } else {
          yield TasksNoInternetState();
        }
      });
    }
  }

When I get navigated to the Tasks page, the BlocBuilder checks the state and handles the building accordingly. I have a Go Back functionality that navigates back to the Courses page:

              onPressed: () {
                serviceLocator<CourseBloc>().add(
                  CoursesPageLoadedEvent(truckNumber: this.truckNumber),
                );
                Navigator.of(context).pop(true);
              },

This fires the similar event for the previous page and it gets re-loaded.

The problem I am facing happens if I want to go to another course and see its tasks. If I tap on another item in the list and therefore fire a new TasksPageLoadedEvent (with new properties) the mapEventToState() doesn't get called at all.

I have had similar issues with BLoC before, but they were regarding the BlocListener and states extending Equatable. That is why I had my events NOT extending Equatable (although I am not sure whether this was the issue here). But still nothing happens.

Here are my Events:

abstract class TaskEvent {
  const TaskEvent();
}

class TasksPageLoadedEvent extends TaskEvent {
  final String truckNumber;
  final int courseId;

  TasksPageLoadedEvent({
    this.truckNumber,
    this.courseId,
  });
}

class TasksFetchedFailureEvent extends TaskEvent {
  final String failure;

  TasksFetchedFailureEvent({
    this.failure,
  });
}

class TasksLoadingEvent extends TaskEvent {}

class TasksReloadingErrorEvent extends TaskEvent {}

class TasksPulledFromServerEvent extends TaskEvent {
  final String courseNumber;
  final String truckNumber;
  final int courseId;

  TasksPulledFromServerEvent({
    @required this.courseNumber,
    @required this.truckNumber,
    @required this.courseId,
  });
}

How should I handle my back-and-forth between the two pages using two BLoCs for each page?

like image 391
Borislav Nanovski Avatar asked Mar 17 '20 13:03

Borislav Nanovski


2 Answers

OK, I found an answer myself!

The problem, of course, as Federick Jonathan implied - the instance of the bloc. I am using a singleton instance created by the flutter package get_it. Which is really useful if you are implementing dependency injection (for a clean architecture for example).

So the one instance was the problem.

Luckily the package has implemented the neat method resetLazySingleton<T>.

Calling it upon going back resets the bloc used in that widget. Therefore when I navigate again to the Tasks page I am working with the same but reset instance of that bloc.

Future<bool> _onWillPop() async {
    serviceLocator.resetLazySingleton<TaskBloc>(
      instance: serviceLocator<TaskBloc>(),
    );

    return true;
  }

I hope this answer would help someone in trouble with singletons, dependency injections and going back and forth within a flutter app with bloc.

like image 190
Borislav Nanovski Avatar answered Nov 15 '22 04:11

Borislav Nanovski


for anyone else who has similar issue:

in case you are listening to a repository stream and looping through emitted object, it cause mapEventToState gets blocked. because the loop never ends.

 Stream<LoaderState<Failure, ViewModel>> mapEventToState(
      LoaderEvent event) async* {
    yield* event.when(load: () async* {
      yield const LoaderState.loadInProgress();
      await for (final Either<Failure, Entity> failureOrItems in repository.getAll()) {
        yield failureOrItems.fold((l) => LoaderState.loadFailure(l),
            (r) => LoaderState.loadSuccess(mapToViewModel(r)));
      }
    });
  }

what you should do instead of await for the stream, listen to stream and then raise another event, and then process the event:

watchAllStarted: (e) async* {
    yield const NoteWatcherState.loadInProgress();
   
    _noteStreamSubscription = _noteRepository.watchAll().listen(
        (failureOrNotes) =>
            add(NoteWatcherEvent.notesReceived(failureOrNotes)));
  },
 
  notesReceived: (e) async* {
    yield e.failureOrNotes.fold(
        (failure) => NoteWatcherState.loadFailure(failure),
        (right) => NoteWatcherState.loadSuccess(right));
  },
like image 45
Hashem Aboonajmi Avatar answered Nov 15 '22 02:11

Hashem Aboonajmi