5

I am using flutter_bloc library to create a verification code page. Here is what I tried to do.

class PhonePage extends StatelessWidget {
  static Route route() {
    return MaterialPageRoute<void>(builder: (_) => PhonePage());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: BlocProvider(
        create: (_) =>
            ValidationCubit(context.repository<AuthenticationRepository>()),
        child: PhoneForm(),
      ),
    );
  }
}

class PhoneForm extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocConsumer<ValidationCubit, ValidationState>(
      listener: (context, state) {
        print('Listener has been called');
        if (state.status.isSubmissionFailure) {
          _showVerificationError(context);
        } else if (state.status.isSubmissionSuccess) {
          _showVerificationSuccess(context);
        }
      },
      builder: (context, state) {
        return Container(
          child: SingleChildScrollView(
            child: Padding(
              padding: EdgeInsets.all(16.0),
              child: Column(
                mainAxisSize: MainAxisSize.max,
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  _HeaderAndTitle(),
                  _VerificationInput(),
                  _VerifyButton()
                ],
              ),
            ),
          ),
        );
      },
    );
  }

  void _showVerificationError(context) {
    Scaffold.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(const SnackBar(content: Text('Validation error')));
  }

  void _showVerificationSuccess(context) {
    Scaffold.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(const SnackBar(
        content: Text('Validation Success'),
        backgroundColor: Colors.green,
      ));
  }
}

...

class _VerifyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<ValidationCubit, ValidationState>(
        builder: (context, state) {
      return RaisedButton.icon(
          color: Colors.blue,
          padding: EdgeInsets.symmetric(horizontal: 38.0, vertical: 12.0),
          textColor: Colors.white,
          icon: state.status.isSubmissionInProgress
              ? Icon(FontAwesomeIcons.ellipsisH)
              : Icon(null),
          label: Text(state.status.isSubmissionInProgress ? '' : 'Verify',
              style: TextStyle(fontSize: 16.0)),
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
          onPressed: state.status.isValid
              ? () => context.bloc<ValidationCubit>().verifyCode()
              : null);
    });
  }
}

Now the verifyCode() function is an async function defined inside ValidationCubit. It emits states with status set to loading, success and failure. However the listener doesn't pick up those changes and show the snackbars. I couldn't figure why? I am also using the Formz library as suggested in the flutter_bloc documentation. Here is the verifyCode part.

Future<void> verifyCode() async {
    if (!state.status.isValidated) return;
    emit(state.copyWith(status: FormzStatus.submissionInProgress));
    try {
      // send validation code to server here
      await _authenticationRepository.loginWithEmailAndPassword(
          email: 'email@email.com', password: '12');
      emit(state.copyWith(status: FormzStatus.submissionSuccess));
    } on Exception {
      emit(state.copyWith(status: FormzStatus.submissionFailure));
    }
  }

The verification code model looks like this:

class ValidationState extends Equatable {
  final VerificationCode verificationCode;
  final FormzStatus status;

  const ValidationState({this.verificationCode, this.status});

  @override
  List<Object> get props => [verificationCode];

  ValidationState copyWith(
      {VerificationCode verificationCode, FormzStatus status}) {
    return ValidationState(
        verificationCode: verificationCode ?? this.verificationCode,
        status: status ?? this.status);
  }
}

And the validation state class is:

class ValidationState extends Equatable {
  final VerificationCode verificationCode;
  final FormzStatus status;

  const ValidationState({this.verificationCode, this.status});

  @override
  List<Object> get props => [verificationCode];

  ValidationState copyWith(
      {VerificationCode verificationCode, FormzStatus status}) {
    return ValidationState(
        verificationCode: verificationCode ?? this.verificationCode,
        status: status ?? this.status);
  }
}
Merhawi Fissehaye
  • 2,482
  • 2
  • 25
  • 39
  • I don't know what bloc states there are. I know that 'listner' is triggered when state is changed from another state.. – KuKu Nov 04 '20 at 12:19
  • is it printing print('Listener has been called'); ?? – xbadal Nov 04 '20 at 12:29
  • please add your State class or States that you have defined. – xbadal Nov 04 '20 at 12:30
  • @xbadal I have updated it to include the state and the model classes. It's not printing the "Listener has been called" when the button is clicked. – Merhawi Fissehaye Nov 04 '20 at 12:37
  • you are not yielding state. so basically state is not changing. this is why its not printing. – xbadal Nov 04 '20 at 12:41
  • But this is cubit. I thought that's why we are emitting. – Merhawi Fissehaye Nov 04 '20 at 12:42
  • I am pretty confident that your problem is the props property of the state. Can you add the status property to the props of your state, and try again ? The listener is called only when the state is changing and because you use the Equatable you have to let it know all your mutable properties in order to tell a state is really equal to another. `@override List get props => [verificationCode, status];` – jorjdaniel Nov 09 '20 at 18:14

4 Answers4

22

I think the problem is your state class.

listener is only called once for each state change (NOT including the initial state) unlike builder in BlocBuilder and is a void function.

Every time when a new state is emitted by the Cubit it is compared with the previous one, and if they are "DIFFERENT", the listener function is triggered.

In you situation, you are using Equatable with only verificationCode as props, which means when two states are compared only the verificationCodes are tested. In this way BLoC consumer thinks that the two states are equal and do not triggers the listener function again.

If you check your verifyCode() function the only changing parameter is status.

In order to fix that add the status property to the props list in your state class.

  @override
  List<Object> get props => [this.verificationCode, this.status];
jorjdaniel
  • 675
  • 5
  • 11
1

if you want to update same state just add one state before calling your updating state like this if you want to update 'Loaded' state again call 'Loading' state before that and than call 'Loaded' state so BlocListener and BlocBuilder will listen to it

Edited I have changed using bloc to cubit for this and cubit can emmit same state continuously and bloclistener and blocbuilder can listen to it

0

I needed to delete a list item from the list and app should pops a pop-up before delete. Somehow you decline the delete pop up via no button or click outside of the pop-up, last state doesn't change. After that, if you want to delete same item, it wasn't trigger cause all parameters are same with the previous state and equatable says its same. To get rid of this issue, you need to define a rand function and put just before your emit state. You need to add a new parameter to your state and you need to add to the props. It works like a charm.

My state;

class SomeDeleteOnPressedState extends SomeState
    with EquatableMixin {
  final int index;
  final List<Result> result;
  final String currentLocation;
  final int rand;/// this is the unique part.

  SomeDeleteOnPressedState({
    required this.index,
    required this.result,
    required this.currentLocation,
    required this.rand,
  });
// don't forget to add rand parameter in props. it will make the difference here.
  @override
  List<Object> get props => <Object>[index, result, currentLocation, rand];
}


and my bloc;

on<SomeDeleteEvent>((event,emit){
int rand =Random().nextInt(100000);
 emit(
            SomeDeleteOnPressedState(
              currentLocation: event.currentLocation,
              index: event.index,
              result: event.result,
              rand: rand,/// every time it will send a different rand, so this state is always will be different.
            ),
          );

});


Hope it helps.

0

I was having this issue as well. I realized I needed to move my BlocBuilder further up the widget tree so that I could listen to a context with an active Bloc.

C RICH
  • 165
  • 9