Flutter value object factory constructor executes twice on Bloc emit

130

Thank you for providing the project using which I could see your problem.

I've detected what the issue is. You are looking for the text factory constructor of EmailAddress, input: in your logs and you are seeing it printed twice in the logs when the user changes their email address by typing, for instance, a new character in the TextField.

The reason you are seeing that message printed to the screen twice is that:

First you are emitting this event:

LoginBloc(this._authFacade): super(LoginState()) {
  on<LoginEmailChanged>((event, emit) async {
    emit(
      state.copyWith(emailAddress: EmailAddress(event.emailAddress))
    );
  });

which in turn calls the copyWith() function of your state that looks like this:

LoginState copyWith({
  EmailAddress? emailAddress,
  Password? password,
  bool? isSubmitting,
  Option<Either<AuthFailure, Unit>>? authFailureOrSuccess
}) {
  return LoginState(
    emailAddress: emailAddress ?? this.emailAddress,
    password: password ?? this.password,
    isSubmitting: isSubmitting ?? this.isSubmitting,
    authFailureOrSuccess: authFailureOrSuccess ?? this.authFailureOrSuccess
  );
}

And this is calling the LoginState() constructor that itself creates another copy of the EmailAddress like so:

LoginState({
  emailAddress,
  password,
  this.isSubmitting = false,
  authFailureOrSuccess
}) : emailAddress = EmailAddress(""),
      password = Password(""),
      authFailureOrSuccess = none();

So even though you think you are calling the LoginState and that should just use your incoming EmailAddress? value, the default constructor is indeed creating another instance of EmailAddress before it uses the one you provide to it.

The solution is to actually fix your LoginState constructor as I've shown you here:

class LoginState {
  final EmailAddress emailAddress;
  final Password password;
  final bool isSubmitting;
  final Option<Either<AuthFailure, Unit>> authFailureOrSuccess;

  LoginState({
    required this.emailAddress,
    required this.password,
    required this.isSubmitting,
    required this.authFailureOrSuccess,
  });
  ...

Then you will have to solve the issue with this line of code which we have in your code base:

LoginBloc(this._authFacade): super(LoginState())

Since LoginState no longer has a default state, you'll need to define one like so:

@injectable
class LoginBloc extends Bloc<LoginEvent, LoginState> {
  final IAuthFacade _authFacade;

  LoginBloc(this._authFacade)
      : super(LoginState(
          emailAddress: EmailAddress(''),
          password: Password(''),
          isSubmitting: false,
          authFailureOrSuccess: none(),
        )) {
    on<LoginEmailChanged>((event, emit) async {
      emit(state.copyWith(emailAddress: EmailAddress(event.emailAddress)));
    });
   ...

And you're good to go after that!

Share:
130
DanteC
Author by

DanteC

Updated on December 19, 2022

Comments

  • DanteC
    DanteC over 1 year

    [UPDATE]: I created a reproduction repo here: https://gitlab.com/dantec204/flutter-app-issue-reproduction

    I'm learning Flutter/Dart and so far I really love working with it, but today I ran into an issue and I can't seem to wrap my head around it.

    I started using Bloc for my user authentication. Whenever the user enters his email address, an EmailChanged event should be emitted. So far so good, everything still works fine.

    But for email addresses, I have this value object class:

    class EmailAddress extends ValueObject<String> {
      @override
      final Either<Failure<String>, String> value;
    
      factory EmailAddress(String input) {
        return EmailAddress._(
          validateEmailAddress(input),
        );
      }
    
      const EmailAddress._(this.value);
    }
    
    Either<Failure<String>, String> validateEmailAddress(String input) {
      const emailRegex = r"""^[a-zA-Z0-9.!#$%&'*\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])?)*$""";
    
      if (RegExp(emailRegex).hasMatch(input)) {
        return right(input);
      } else {
        return left(Failure(input));
      }
    }
    

    Problem here is that somehow, whenever the bloc event gets emitted, this factory constructor gets executed twice. First it receives the right input value, but the second time the input value is just a blank string. And thus my state is incorrect.

    This is what the bloc looks like:

    @injectable
    class LoginBloc extends Bloc<LoginEvent, LoginState> {
      final IAuthFacade _authFacade;
    
      LoginBloc(this._authFacade): super(LoginState()) {
        on<LoginEmailChanged>((event, emit) async {
          emit(
            state.copyWith(emailAddress: EmailAddress(event.emailAddress))
          );
        });
      }
    }
    

    the bloc state:

    class LoginState {
      final EmailAddress emailAddress;
      final Password password;
      final bool isSubmitting;
    
      LoginState({
        emailAddress,
        password,
        this.isSubmitting = false
      }) : emailAddress = EmailAddress(""),
           password = Password("");
    
      LoginState copyWith({
        EmailAddress? emailAddress,
        Password? password,
        bool? isSubmitting
      }) {
        return LoginState(
          emailAddress: emailAddress ?? this.emailAddress,
          password: password ?? this.password,
          isSubmitting: isSubmitting ?? this.isSubmitting,
        );
      }
    }
    

    and this is the onChanged event from the TextFormField:

    onChanged: (value) =>
         context.read<LoginBloc>().add(LoginEmailChanged(emailAddress: value)),
    
    abstract class LoginEvent {}
    
    class LoginEmailChanged extends LoginEvent {
      final String emailAddress;
    
      LoginEmailChanged({ required this.emailAddress });
    }
    

    I really hope someone can help me out here because I've been looking on the internet and thinking about it for almost the entire day...

    • Vandad Nahavandipoor
      Vandad Nahavandipoor about 2 years
      Have you tried debugging your application and actually finding the backtrace? This should give you enough information about what is causing the double-entrance.
    • DanteC
      DanteC about 2 years
      Yes, I did try debugging but still don't see the problem. The 'on' event listener seems to be executed only once, but then once it reaches the emit(state.copyWith(...)) the EmailAddress factory gets executed twice...
    • Vandad Nahavandipoor
      Vandad Nahavandipoor about 2 years
      Is it possible for you to isolate the problematic part of your code into a new project that you can perhaps share somewhere? It would be great to be able to have a look at that code and help you that way.
    • DanteC
      DanteC about 2 years
      Yes, I created a Gitlab repo. You can get the project here: gitlab.com/dantec204/flutter-app-issue-reproduction (I'm completely new to Flutter & Dart by the way, so all tips are welcome if you have any :) )
    • Vandad Nahavandipoor
      Vandad Nahavandipoor about 2 years
      Great job in creating a reproducible repository which I could use to see your problem. I've found the issue and posted it as an answer.
  • DanteC
    DanteC about 2 years
    I'm sorry about my late response. Your answer and explanation helped me out and I can now see what I did wrong. Thanks a lot!
  • Vandad Nahavandipoor
    Vandad Nahavandipoor about 2 years
    You're more than welcome. Thank you for taking the time to accept the answer as well.