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.
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:
BuildContext
to be able to test field models once I add localization.