bloc state is the same after coming back to a page even though it was explicitly changed

Issue

I have a widget that utilizes CourseBloc

class CourseBloc extends Bloc<CourseEvent, CourseState> {
  final GetCoursesQuery getCoursesQuery;
  final GetSettingsQuery getSettingsQuery;
  final PullCoursesFromServerCommand pullCoursesFromServerCommand;
  final UpdateTasksToServerCommand updateTasksToServerCommand;
  final GetNetworkInfoQuery getNetworkInfoQuery;

  CourseBloc({
    @required GetCoursesQuery getCoursesQuery,
    @required GetSettingsQuery getSettingsQuery,
    @required PullCoursesFromServerCommand pullCoursesFromServerCommand,
    @required UpdateTasksToServerCommand updateTasksToServerCommand,
    @required GetNetworkInfoQuery getNetworkInfoQuery,
  })  : assert(getCoursesQuery != null),
        assert(getSettingsQuery != null),
        assert(pullCoursesFromServerCommand != null),
        assert(updateTasksToServerCommand != null),
        assert(getNetworkInfoQuery != null),
        this.getCoursesQuery = getCoursesQuery,
        this.getSettingsQuery = getSettingsQuery,
        this.pullCoursesFromServerCommand = pullCoursesFromServerCommand,
        this.updateTasksToServerCommand = updateTasksToServerCommand,
        this.getNetworkInfoQuery = getNetworkInfoQuery;

  @override
  CourseState get initialState => CourseInitialState();

  @override
  Stream<CourseState> mapEventToState(
    CourseEvent event,
  ) async* {
    if (event is CoursesReturnedFromTasksEvent) {
      yield CourseInitialState();
    } else if (event is CoursesPageLoadedEvent) {
      yield CourseLoadingState();

      final getCoursesEither = await getCoursesQuery(
        GetCoursesParams(
          truckNumber: event.truckNumber,
        ),
      );

      yield* getCoursesEither.fold((failure) async* {
        yield CourseFetchedStateFailureState(error: "coursesDatabaseError");
      }, (result) async* {
        if (result != null) {
          final getSettingsEither = await getSettingsQuery(NoQueryParams());

          yield* getSettingsEither.fold((failure) async* {
            yield CourseFetchedStateFailureState(error: "coursesDatabaseError");
          }, (settingsResult) async* {
            if (result != null) {
              final networkInfoEither =
                  await this.getNetworkInfoQuery(NoQueryParams());

              yield* networkInfoEither.fold((failure) async* {
                yield CourseErrorState();
              }, (success) async* {
                yield CourseFetchedState(
                  settings: settingsResult,
                  courses: result,
                  isThereInternet: success,
                );
              });
            } else {
              yield CourseFetchedStateFailureState(
                  error: "coursesFetchFromDatabaseError");
            }
          });
        } else {
          yield CourseFetchedStateFailureState(
              error: "coursesFetchFromDatabaseError");
        }
      });
    } else if (event is CoursesRefreshButtonPressedEvent) {
      yield CourseLoadingState();

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

      yield* networkInfoEither.fold((failure) async* {
        yield CourseErrorState();
      }, (success) async* {
        if (success) {
          final updateTasksToServerEither = await updateTasksToServerCommand(
              UpdateTasksParams(truckNumber: event.truckNumber));

          yield* updateTasksToServerEither.fold((failure) async* {
            yield CourseFetchedStateFailureState(error: "coursesDatabaseError");
          }, (result) async* {
            final pullCoursesFromServerEither =
                await pullCoursesFromServerCommand(
                    PullCoursesParams(truckNumber: event.truckNumber));

            yield* pullCoursesFromServerEither.fold((failure) async* {
              yield CourseFetchedStateFailureState(
                  error: "coursesDatabaseError");
            }, (result) async* {
              if (result != null) {
                yield CoursePulledFromServerState();
              } else {
                yield CourseFetchedStateFailureState(
                    error: "coursesFetchFromDatabaseError");
              }
            });
          });
        } else {
          yield CourseNoInternetState();
        }
      });
    } else if (event is CoursesRefreshFromTasksButtonPressedEvent) {
      serviceLocator<TaskBloc>().add(
        TasksLoadingEvent(),
      );

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

      yield* networkInfoEither.fold((failure) async* {
        serviceLocator<TaskBloc>().add(
          TasksReloadingErrorEvent(),
        );
      }, (success) async* {
        if (success) {
          final updateTasksToServerEither = await updateTasksToServerCommand(
              UpdateTasksParams(truckNumber: event.truckNumber));

          yield* updateTasksToServerEither.fold((failure) async* {
            yield CourseFetchedStateFailureState(error: "coursesDatabaseError");
          }, (result) async* {
            final pullCoursesFromServerEither =
                await pullCoursesFromServerCommand(
                    PullCoursesParams(truckNumber: event.truckNumber));

            yield* pullCoursesFromServerEither.fold((failure) async* {
              serviceLocator<TaskBloc>().add(
                TasksFetchedFailureEvent(failure: "coursesDatabaseError"),
              );
            }, (result) async* {
              if (result != null) {
                serviceLocator<TaskBloc>().add(
                  TasksPulledFromServerEvent(
                    truckNumber: event.truckNumber,
                    courseNumber: event.courseNumber,
                    courseId: event.courseId,
                  ),
                );
              } else {
                serviceLocator<TaskBloc>().add(
                  TasksFetchedFailureEvent(
                      failure: "coursesFetchFromDatabaseError"),
                );
              }
            });
          });
        } else {
          yield CourseNoInternetState();
        }
      });
    }
  }
}

I use BlocBuilder and BlocListener in the widget page as follows:

BlocBuilder<CourseBloc, CourseState>(
   builder: (context, state) {
     if (state is CourseFetchedState) {
     // here I have logic if the course is fetched
   }
...
),

BlocListener<CourseBloc, CourseState>(
   listener: (context, state) {
     if (state is CourseNoInternetState) {
     ...
   }
...
),

At some point I am navigating away of this widget. Then I want to come back to this first (Course) widget. I do:

serviceLocator<CourseBloc>().add(
     CoursesReturnedFromTasksEvent(),
);

serviceLocator.resetLazySingleton<TaskBloc>(
     instance: serviceLocator<TaskBloc>(),
);

Navigator.of(context).pop(true);

This tells the CourseBloc to expect a CoursesReturnedFromTasksEvent() , resets the new TaskBloc (because I am not on this page anymore and I don’t need to know what state is it at) and pops the current context.

Then I am navigated back. The CourseBloc’s mapping method works with the new state and according to the ‘if’ it yields:

if (event is CoursesReturnedFromTasksEvent) {
  yield CourseInitialState();
}

But the Builder in the Course page is at the previous state. CourseFetchedState. And it has not ‘taken’ the new state (Initial).

Any ideas why that might happen?

Here are the States:

abstract class CourseState {
  CourseState();
}

class CourseInitialState extends CourseState {}

class CourseReturnedFromTasksState extends CourseState {}

class CourseLoadingState extends CourseState {}

class CourseErrorState extends CourseState {}

class CourseNoInternetState extends CourseState {}

class CoursePulledFromServerState extends CourseState {}

class CourseFetchedState extends CourseState {
  final SettingsAggregate settings;
  final List<CourseAggregate> courses;
  final bool isThereInternet;

  CourseFetchedState({
    @required this.settings,
    @required this.courses,
    @required this.isThereInternet,
  });
}

class CourseFetchedStateFailureState extends CourseState {
  final String error;

  CourseFetchedStateFailureState({@required this.error});
}

And Events:

abstract class CourseEvent {
  CourseEvent();
}

class CoursesRefreshButtonPressedEvent extends CourseEvent {
  final String truckNumber;

  CoursesRefreshButtonPressedEvent({@required this.truckNumber});
}

class CoursesRefreshFromTasksButtonPressedEvent extends CourseEvent {
  final String courseNumber;
  final String truckNumber;
  final int courseId;

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

class CoursesReturnedFromTasksEvent extends CourseEvent {}

class CoursesPageLoadedEvent extends CourseEvent {
  final String truckNumber;

  CoursesPageLoadedEvent({
    this.truckNumber,
  });
}

EDIT
Here is how the Bloc is provided:

Column buildBody(BuildContext context) {
    return Column(
      children: <Widget>[
        BlocProvider(
          create: (_) => serviceLocator<CourseBloc>(),
          child: BlocBuilder<CourseBloc, CourseState>(
            builder: (context, state) {
              if (state is CourseFetchedState) {
              ...
              }
            ...
            }
           ),
          ),
         ],
        );
       }

The serviceLocator() is instantiated only once, at app startup. It registers all instances needed throughout the app life in order to achieve dependency injection. Here is the CourseBloc registration:

import 'package:get_it/get_it.dart';

final serviceLocator = GetIt.instance;
...
serviceLocator.registerLazySingleton(
    () => CourseBloc(
      getCoursesQuery: serviceLocator(),
      getSettingsQuery: serviceLocator(),
      pullCoursesFromServerCommand: serviceLocator(),
      updateTasksToServerCommand: serviceLocator(),
      getNetworkInfoQuery: serviceLocator(),
    ),
  );
...

Solution

I am not really familiar with GetIt, but from what I can tell:

  1. You make a singleton of CourseBloc with serviceLocator.
  2. You use BlocProvider to provide that singleton to the relevant context (Resulting in 2 instances of CourseBloc).
  3. Your BlocBuilder uses the instance provided via BlocProvider because you don’t explicitly tell via bloc parameter (It will look up the widget tree to find CourseBloc within the same context).
  4. You use the instance of CourseBloc made by serviceLocator to add CoursesReturnedFromTasksEvent().

Try adding event the usual way via BlocProvider.of, but you still need manually dispose the CourseBloc singleton made by serviceLocator

BlocProvider.of<CourseBloc>(context).add(
  CoursesReturnedFromTasksEvent(),
);

Answered By – Federick Jonathan

Answer Checked By – Willingham (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *