felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.8k stars 3.39k forks source link

BlocBuilder is not rebuilding #2433

Closed vt-dev0 closed 3 years ago

vt-dev0 commented 3 years ago

Hello. While trying to find answer for my question on google I saw few people posting here for some advice, hence why I am posting here as well.

Here is the main idea of my current implementation:

HomeScreen will contain NavigationDrawer. NavigationDrawer will use 2 Blocs:

DrawerNavigationBloc can emit either DrawerProfilePageState or DrawerTasksPageState which will build either page correspondingly.

There is no issue when it comes to building different pages (Profile or Tasks), however, that is not all. TasksPage has to have a parameter of int taskType passed to it in order to fetch corresponding list of Tasks from the server which will then be displayed on screen.

Screenshot_1619695958

That's where the issue currently lies. Whenever I load one TasksPage with any taskType and then simply pick another in NavDrawer the UI is not being rebuilt, in fact, there isn't even any event being triggered in TasksBloc.

I am not really sure what exactly is the problem here, but maybe it has something to do with BlocProvider in HomeScreen.

Code is below:

HomeScreen:

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<DrawerBloc>(
          create: (context) => DrawerBloc(),
        ),
        BlocProvider<DrawerNavigationBloc>(
          create: (context) => DrawerNavigationBloc(),
        ),
      ],
      child: Scaffold(
        drawer: NavigationDrawer(),
        appBar: PreferredSize(
          preferredSize: const Size.fromHeight(50),
          child: PlatformAppBar(),
        ),
        body: BlocBuilder<DrawerNavigationBloc, DrawerNavigationState>(
          builder: (context, state) {
            if (state is DrawerTasksPageState) {
              return BlocProvider<TasksBloc>(
                create: (context) => TasksBloc(state.taskType),
                child: TasksPage(),
              );
            } else if (state is DrawerProfilePageState) {
              return ProfilePage();
            } else {
              // todo -> make this more meaningful
              return Container();
            }
          },
        ),
      ),
    );
  }
}

TasksPage

class TasksPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: BlocConsumer<TasksBloc, TasksState>(
        listener: (context, state) {
          //TODO -> implement listener
        },
        builder: (context, state) {
          if (state is TasksSuccessState) {
            return ListView.builder(
              itemCount: state.tasks.length,
              itemBuilder: (context, index) {
                return Card(
                  child: Text(state.tasks[index].address),
                );
              },
            );
          } else if (state is TasksLoadingState) {
            return Center(
              child: CircularProgressIndicator(),
            );
          } else {
            return Container(
              child: Text('just container'),
            );
          }
        },
      ),
    );
  }
}

TasksBloc

class TasksBloc extends Bloc<TasksEvent, TasksState> {
  TasksBloc(this.taskType) : super(TasksInitialState()) {
    add(TasksLoadEvent(taskType));
  }
  final int taskType;

  final HomeRepository _homeRepository = HomeRepository();

  @override
  Stream<TasksState> mapEventToState(TasksEvent event) async* {
    if (event is TasksLoadEvent) {
      log('tasks load event');
      yield TasksLoadingState();

      final DataState _dataState = await _homeRepository.getTasks(event.taskType);

      _dataState.maybeWhen(
          success: (data) => emit(TasksSuccessState(Task.parseListOfTasks(data))),
          failure: (data) => emit(TasksErrorState(errorMessage: data)),
          unauthenticated: () => emit(TasksUnauthenticatedState()),
          networkError: (data) => emit(TasksErrorState(errorMessage: 'error_network_Body'.tr(namedArgs: {'details': data}))),
          unknownError: (data) => emit(TasksErrorState(errorMessage: 'error_unexpected_Body'.tr(namedArgs: {'details': data}))),
          orElse: () => emit(TasksErrorState()));
    }
  }
}

NavigationDrawer

class NavigationDrawer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: Column(
        children: [
          MediaQuery.removePadding(
            context: context,
            child: Expanded(
              child: BlocBuilder<DrawerBloc, DrawerState>(
                builder: (context, state) {
                  if (state is DrawerSuccess) {
                    return ListView.builder(
                      itemCount: state.taskTypes.length + 1,
                      itemBuilder: (context, index) {
                        if (index == 0) {
                          return Column(
                            children: [
                              DrawerHeader(
                                child: Image.asset('assets/images/logo.png'),
                              ),
                              Divider(),
                              ListTile(
                                title: Text(tr('profile')),
                                leading: Icon(PlatformIcons(context).person),
                                onTap: () {
                                  context.read<DrawerNavigationBloc>().add(DrawerProfilePageEvent());
                                  Navigator.pop(context);
                                },
                              ),
                              Divider(),
                            ],
                          );
                        }
                        index--;
                        return ListTile(
                          title: Text(state.taskTypes[index].name),
                          leading: Icon(IconData(state.taskTypes[index].appIcon, fontFamily: 'MaterialIcons')),
                          onTap: () {
                            context.read<DrawerNavigationBloc>().add(DrawerTasksPageEvent(state.taskTypes[index].id));
                            Navigator.pop(context);
                          },
                        );
                      },
                    );
                  } else {
                    return Container();
                  }
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

DrawerBloc

class DrawerBloc extends Bloc<DrawerEvent, DrawerState> {
  DrawerBloc() : super(DrawerInitial())  {
    add(DrawerLoad());
  }

  final HomeRepository _homeRepository = HomeRepository();

  @override
  Stream<DrawerState> mapEventToState(DrawerEvent event) async* {
    if (event is DrawerLoad) {
      yield DrawerLoading();

      final DataState _dataState = await _homeRepository.getTypes();

      _dataState.maybeWhen(
        success: (data) {
          emit(DrawerSuccess(TaskType.parseListOfTypes(data)));
        },
        unsynchronized: () => emit(DrawerUnsynchronized()),
        orElse: () => emit(DrawerError())
      );
    }
  }
}

DrawerNavigationBloc

class DrawerNavigationBloc extends Bloc<DrawerNavigationEvent, DrawerNavigationState> {
  DrawerNavigationBloc() : super(DrawerProfilePageState());

  @override
  Stream<DrawerNavigationState> mapEventToState(DrawerNavigationEvent event) async* {
    if (event is DrawerProfilePageEvent) {
      yield DrawerProfilePageState();
    } else if (event is DrawerTasksPageEvent) {
      log('drawer navigation event');
      yield DrawerTasksPageState(event.taskType);
    }
  }
}

============================================== Please help me out here.

II11II commented 3 years ago

Hi @vitaliy-t 🖐 To update the state in the bloc, you should use yield instead of emit. Here you can see

vt-dev0 commented 3 years ago

Hi @vitaliy-t raised_hand_with_fingers_splayed To update the state in the bloc, you should use yield instead of emit. Here you can see

Hey. Thank you very much for your response.

I updated my TasksBloc and replaced my emit uses with yield. The code looks as following now:

  TasksBloc(this.taskType) : super(TasksInitialState()) {
    add(TasksLoadEvent(taskType));
  }

  final int taskType;

  final HomeRepository _homeRepository = HomeRepository();

  @override
  Stream<TasksState> mapEventToState(TasksEvent event) async* {
    if (event is TasksLoadEvent) {
      log('tasks load event');
      yield TasksLoadingState();

      final DataState _dataState = await _homeRepository.getTasks(event.taskType);

      if (_dataState is Success) {
        yield TasksSuccessState(Task.parseListOfTasks(_dataState.data));
      } else if (_dataState is Failure) {
        yield TasksErrorState(errorMessage: _dataState.message);
      } else if (_dataState is Unauthenticated) {
        yield TasksUnauthenticatedState();
      } else if (_dataState is NetworkError) {
        yield TasksErrorState(errorMessage: 'error_network_Body'.tr(namedArgs: {'details': _dataState.message}));
      } else if (_dataState is UnknownError) {
        yield TasksErrorState(errorMessage: 'error_unexpected_Body'.tr(namedArgs: {'details': _dataState.message}));
      } else {
        yield TasksErrorState();
      }
  }
}

Unfortunately this did not solve my problem and the same issue still persists.

vt-dev0 commented 3 years ago

I created a sample that has this exact error. Here is the link:

https://github.com/vitaliy-t/flutter-bloc-builder-issue-sample

felangel commented 3 years ago

@vitaliy-t I took a look at it looks like your models/states aren't using Equatable properly. For example https://github.com/vitaliy-t/flutter-bloc-builder-issue-sample/blob/74e3fbe6b4abf5508c35893b9a82d512f7a5d7dd/lib/homescreen/model/task/Task.dart#L52 is missing a bunch of props. More information can be found at https://bloclibrary.dev/#/faqs?id=state-not-updating. Let me know if that helps 👍

vt-dev0 commented 3 years ago

@vitaliy-t I took a look at it looks like your models/states aren't using Equatable properly. For example https://github.com/vitaliy-t/flutter-bloc-builder-issue-sample/blob/74e3fbe6b4abf5508c35893b9a82d512f7a5d7dd/lib/homescreen/model/task/Task.dart#L52 is missing a bunch of props. More information can be found at https://bloclibrary.dev/#/faqs?id=state-not-updating. Let me know if that helps +1

Thank you very much for your response. To simplify things a bit further, I removed all fields from Task model and left only 2 that are inside Equatable property list. I also manually ensured (for testing purposes) that all the items compared are different to completely eliminate that as an issue.

Also, I tried few things on my own. As you will be able to see in this code: https://github.com/vitaliy-t/flutter-bloc-builder-issue-sample/blob/master/lib/homescreen/view/HomeScreen.dart To keep track of how things are going in terms of app flow I added logging.

So inside HomeScreen we have 2 events:

  1. whenever any state change of DrawerNavigationState is received, we print it out to console to see what we got.
  2. then we make extra step to ensure that we got correct state by logging from our if statement.

That covers my navigation issues which there seem to be none.

Then, I added another log to TasksBloc, the one that is responsible for yielding TasksPage. Like this: https://github.com/vitaliy-t/flutter-bloc-builder-issue-sample/blob/master/lib/homescreen/view/taskspage/bloc/TasksBloc.dart This will tell us that the event was received and will soon be mapped to state. That's where something strange happens.

When I switch from ProfilePage to TasksPage of any type we get the following log:

[log] DrawerTasksPageState(1)
[log] returning tasks page
[log] tasks bloc event is received

Awesome.

  1. We got the line from HomeScreen showing what kind of event we got.
  2. Confirmed by info from if statement
  3. Another log from TasksBloc which shows us that the event was sent and received.

Then, we I try and move from one type of TasksPage to another, the following log is sent out:

[log] DrawerTasksPageState(2)
[log] returning tasks page

Which means that event never gets sent.

I tried going through this with debugger and the issue is exactly that - the TasksBloc never gets used when switching between types of TasksPage and only when switching from ProfilePage.

vt-dev0 commented 3 years ago

After playing around with debugger a little bit more I figured what was the issue.

For unknown to me reason, BlocBuilder, when navigating between different TasksPage types instead of returning newly created instance of TasksPage returns an old one. Since the event is called only in initState, and the initState is not called when working with the same instance, the UI does not update even though the new taskType is passed.

I fixed it by adding event before returning TasksPage which seems to do the trick, but I would like to know if perhaps there is a better solution that would involve recreating TasksPage widgets completely instead of simply sending an event.

code looks as following:

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<DrawerBloc>(
          create: (context) => DrawerBloc(),
        ),
        BlocProvider<DrawerNavigationBloc>(
          create: (context) => DrawerNavigationBloc(),
        ),
        BlocProvider(
          create: (context) => TasksBloc(),
        )
      ],
      child: Scaffold(
        drawer: NavigationDrawer(),
        appBar: PreferredSize(
          preferredSize: const Size.fromHeight(50),
          child: PlatformAppBar(),
        ),
        body: BlocBuilder<DrawerNavigationBloc, DrawerNavigationState>(
          builder: (context, state) {
            log(state.toString());
            if (state is DrawerTasksPageState) {
              log('returning tasks page');
              context.read<TasksBloc>().add(TasksLoadEvent(state.taskType));
              return new TasksPage(state.taskType);
            } else if (state is DrawerProfilePageState) {
              log('returning profile page');
              return ProfilePage();
            } else {
              log('returning empty contaienr page');
              // todo -> make this more meaningful
              return Container();
            }
          },
        ),
      ),
    );
  }
}
felangel commented 3 years ago

If you want to recreate the TasksPage you could use a UniqueKey():

TestsPage(key: UniqueKey(), taskType: state.taskType);

But usually this is a symptom of a larger architectural issue imo. Closing for now but feel free to join the bloc discord if you want to continue the discussion 👍