Using BlocListener with Navigator PushNamed is causing ambiguity

Issue

I am facing issue while using bloc listeners for navigation

I have five screen for navigation and I am using BlocConsumer in every file. I am looking in flutter inspector for granular view. I am using blocListener for navigation. when I pushNamed 1st screen everything works fine and I navigate to second screen (1st screen is added to navigation stack). Now I am in 2nd screen, when I pressed to move to the 3rd screen two 2nd screens and then 3rd screen added, The stack should be like this ( 1st screen , 2nd screen, 3rd screen ) but unfortunately it is like ( 1st screen , 2nd screen, 2nd screen, 3rd screen ). Now when I am on 3rd screen and wanted to add 4th screen is stack but 2nd screen is added then 3rd screen is added two times then 4th screen is added. The stack should be like this ( 1st screen , 2nd screen, 3rd screen, 4th screen ) but unfortunately it is like ( 1st screen , 2nd screen, 2nd screen, 3rd screen, 2nd screen, 3rd screen, 3rd screen, 4th screen ). So instead of 4 screens I have 8 screens in navigation stack.

This the the pattern which I am using in all the files.

This is where I am creating bloc instance and closing it.

class MyAppRoutes {
  FieldsBloc _fieldsBloc = FieldsBloc();

  Route onGenerateRoute(RouteSettings routeSettings) {
    try {
      switch (routeSettings.name) {

        case LandingPage.routeName:
          return MaterialPageRoute(builder: (_) => LandingPage());

        case CategoryPage.routeName:
          return MaterialPageRoute(
              builder: (context) => BlocProvider.value(
                    value: _fieldsBloc,
                    child: CategoryPage(),
                  ));

        case ExpertisePage.routeName:
          return MaterialPageRoute(
              builder: (context) => BlocProvider.value(
                    value: _fieldsBloc,
                    child: ExpertisePage(),
                  ));

        case ExpertiseLevelPage.routeName:
          return MaterialPageRoute(
              builder: (context) => BlocProvider.value(
                    value: _fieldsBloc,
                    child: ExpertiseLevelPage(),
                  ));

        case EducationPage.routeName:
          return MaterialPageRoute(
              builder: (context) => BlocProvider.value(
                    value: _fieldsBloc,
                    child: EducationPage(),
                  ));

        default:
          return null;
      }
    } catch (e) {
      print(e);
    }
  }

  void dispose() async {
    _fieldsBloc.close();
  }
}

This is the widget which I am using in every file.

BlocConsumer<FieldsBloc, FieldsState>(builder: (context, state) {
              if (state is FieldsInitial) {
                return Container();
              } else if (state is FieldLoadingState) {
                return Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: getCircularProgress(context),
                );
              } else if (state is FieldSuccessfulState) {
                return Container();
              } else if (state is FieldUnsuccessfulState) {
                return Padding(
                    padding: const EdgeInsets.all(15.0),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Icon(
                          Icons.error,
                          color: Colors.red,
                        ),
                        SizedBox(
                          width: 5.0,
                        ),
                        Expanded(
                            child: TextStyleRes.textStyleFont1(
                                textColor: Colors.red,
                                text: state.message,
                                fontSize: 12,
                                fontWeight: FontWeight.w700)),
                      ],
                    ));
              }
              return Container();
            }, listener: (context, state) {
              if (state is FieldSuccessfulState)
                return SchedulerBinding.instance.addPostFrameCallback((_) {
                  Navigator.of(context).pushNamed(ExpertisePage.routeName);
                });
            }),

This is the Bloc Event which is being triggered.

abstract class FieldsEvent {}

class NextButtonEventScreen3 extends FieldsEvent {
  List<String> categories;

  NextButtonEventScreen3(this.categories);
}

class NextButtonEventScreen4 extends FieldsEvent {
  List skills;

  NextButtonEventScreen4(this.skills);
}

class NextButtonEventScreen5 extends FieldsEvent {
  String expert;

  NextButtonEventScreen5(this.expert);
}

This is the bloc.

if (event is NextButtonEventScreen3) {
       if (event.categories.isNotEmpty) {
         yield FieldLoadingState();
         categories = event.categories;

         yield FieldSuccessfulState();
       } else
         throw ('Please choose at least 1 category');
     }
     //=====================SignUpScreen4===========================
     else if (event is NextButtonEventScreen4) {
       if (event.skills.isNotEmpty) {
         yield FieldLoadingState();
         skills = event.skills;

         yield FieldSuccessfulState(_updateModel());
       } else
         throw ('Please provide at least 1 skill');
     }
     //=====================SignUpScreen5===========================
     else if (event is NextButtonEventScreen5) {
       expert = event.expert;

       yield FieldSuccessfulState();
     } 

and these are the states in bloc

abstract class FieldsState {
 String message;
}

class FieldsInitial extends FieldsState {}
class FieldLoadingState extends FieldsState {}


class FieldSuccessfulState extends FieldsState {
 var data;

 FieldSuccessfulState([this.data]);
}


class FieldUnsuccessfulState extends FieldsState {
 String message;

 FieldUnsuccessfulState({this.message});
}

Solution

The previous pages on the navigation stack are still there, listening to bloc events. When the state becomes FieldsSuccessfulState on the second screen, both listeners see this and try to navigate to the next screen.

To ensure that only the current screen will react to FieldsSuccessfulState, there are two options I can think of:

  • Split FieldsSuccessfulState into multiple outcomes (either different classes or an added field) and make each screen only react to its own success state.

  • Check ModalRoute.of(context).isCurrent before navigating to the next screen. This can be done in the listener itself or the listenWhen parameter.

Answered By – Nitrodon

Answer Checked By – David Goodson (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published.