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

2

7

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?

Forefront answered 17/3, 2020 at 13:17 Comment(8)
I suggest you to use this plugin pub.dev/packages/flutter_blocIsland
But I am, this is the plugin I am using.Forefront
Does it still navigate properly to TaskPage? If yes, is the blocbuilder<TaskBloc> still at the same state or not? How do you know the mapEventToState isn't get called at all?Prism
Hi! I was hoping you to see my issue :) Yes, it navigates. I put a breakpoint. The BlocBuilder<TaskBloc> is with the last state before the "go back to previous page" action.Forefront
haha.. don't expect much from me. I am not sure but I assume serviceLocator provides an instance of TaskBloc. With that said, how do you implement your BlocBuilder because as far as I can tell you don't use BlocProvider to provide TaskBloc instance. Maybe what happened was it used different instance of TaskBloc after navigating back from TaskPagePrism
I thought the same... but I use package:get_it/get_it.dart and I instantiate the bloc like this: serviceLocator.registerLazySingleton( () => TaskBloc( getTasksQuery: serviceLocator(), getNetworkInfoQuery: serviceLocator(), ), ); And this makes one (singleton) instance only. I am confused.Forefront
The "previous" bloc, the CourseBloc, is designed the same way and when I am doing the pop from Tasks Page, the mapEventToState of CourseBloc is hit. It is probably something with the instance, but I am clueless since yesterday.Forefront
Ha! I found a way to fix it! It is to do - as you said - with the instance, of course. The package I use has a way of resetting a singleton instance. That did the trick! Thanks!Forefront
F
3

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.

Forefront answered 18/3, 2020 at 9:0 Comment(2)
Hello, How did you resolve the problem. I'm having the same issue, I'm building the dependency injection with get it, but when i try and use resetLazySingleton I get a error.Express
What is the error you get? Maybe its text / message will help?Forefront
P
1

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));
  },
Plattdeutsch answered 6/4, 2021 at 8:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.