Search code examples
flutterunit-testingdartarchitecturelocalization

Is it bad practice to store/translate static error strings in a form field model?


I'm building a Flutter app where I have a sign-up form that will throw an error if the user inputs an invalid email.

I have the following model for the email form field (using formz package):

enum EmailFieldValidationError { empty, invalid }

class EmailField extends FormzInput<String, EmailFieldValidationError> {
  const EmailField.pure() : super.pure('');

  const EmailField.dirty([String value = '']) : super.dirty(value);

  static final RegExp _emailRegExp = RegExp(
    r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$',
  );

  @override
  EmailFieldValidationError? validator(String? value) {
  }

  String? get errorText {
    if (error != null && !pure && value.isNotEmpty) {
      switch (error) {
        case EmailFieldValidationError.invalid:
          return 'Invalid email.';
        default:
          return null;
      }
    }
    return null;
  }
}

This makes things very simple for now within my widgets when I want to conditionally show error messages under a TextFormField(), but this seems to me like bad practice when considering how localization (which I would like to add to the app later) in flutter would require me to use the BuildContext to build the error strings instead. Like so:

  String? buildErrorText(BuildContext context) {
    if (error != null && !pure && value.isNotEmpty) {
      switch (error) {
        case EmailFieldValidationError.invalid:
          return AppLocalizations.of(context)!.emailFieldInvalidValueError;
        default:
          return null;
      }
    }
    return null;
  }

Given that I'd need to instantiate a BuildContext and configure it with my localization class just to be able to test this form field model, I think that there is poor separation of concerns here.

I thought it would be a good idea at first to generate error texts for my form field models within the class itself. This was primarily to avoid repeated code. But after localization became a nice-to-have for my app, this became a problem, and I'm running into analysis paralysis reading/considering different options for spearating the static string generation from the model class while avoiding my original problem of repeated code.

Any ideas would be much appreciated here.


Solution

  • I worked out a simple solution eventually and since I didn't get any viable answers, here is my solution. It would be nice if anyone could validate or give feedback to further improve it.

    I basically created a new "common" widget to simply render a TextFormField based on the given EmailField value. If the current value has any errors, it will render an appropriate error string based on the error type:

    class CommonEmailFormField extends StatelessWidget {
      const CommonEmailFormField({
        Key? key,
        required this.email,
        this.onChanged,
      }) : super(key: key);
    
      final EmailField email;
      final Function(String)? onChanged;
    
      String? get _errorText {
        if (email.error != null && !email.pure && email.value.isNotEmpty) {
          switch (email.error) {
            case EmailFieldValidationError.invalid:
              return 'Invalid email.';
            default:
              return null;
          }
        }
        return null;
      }
    
      @override
      Widget build(BuildContext context) {
        return TextFormField(
          initialValue: email.value,
          onChanged: onChanged,
          decoration: InputDecoration(
            labelText: 'Email *',
            errorText: _errorText,
          ),
        );
      }
    }
    

    I've simplified the snippet to be concise and to the point. You can take a look at the actual implementation here.

    Once I start adding localization, it will require me to change the _errorText getter read the static localized strings from the BuildContext, but that's still code that drives UI behavior and I maintain separation of concerns.

    Regardless, this solution solves my main problems of:

    1. Unnecessary complexity in testing. Now I don't need to set up a UI BuildContext to be able to test field models once I add localization.
    2. Repeated code. I can just reuse this common widget wherever else I need.
    3. Separation of concerns. Code that drives specifically UI/View behavior is nicely separated from code that drives Model behavior. This in-turn simplifies testing.