Input validation is a business logic so we should hide this process in the domain layer. as discussed here
I do it like this
Login validator
class LoginValidator extends Validator {
String email;
String password;
LoginValidator(this.email, this.password);
@override
void validate(Function() success, Function(List<Failure>) errors) {
List<Failure> failures = [];
if (email.trim().isEmpty) {
failures.add(const EmailValidationFailure('Email is required'));
} else if (!validator.isEmail(email)) {
failures.add(const EmailValidationFailure());
}
if (password.trim().isEmpty) {
failures.add(const PasswordValidationFailure('Password is required'));
}
if (failures.isNotEmpty) {
errors(failures);
} else {
success();
}
}
}
And I created a failure class for each input field
class EmailValidationFailure extends Failure {
const EmailValidationFailure([String message = 'Email is invalid'])
: super(message);
}
class PasswordValidationFailure extends Failure {
const PasswordValidationFailure([String message = 'Incorrect password'])
: super(message);
}
And I use the validator in the use case
class LoginUseCaseInteractor implements LoginUseCaseInputPort {
final AccountRepository _repository;
final LoginUseCaseOutputPort outputPort;
LoginUseCaseInteractor(this._repository, this.outputPort);
@override
void login(LoginParams params) {
LoginValidator(params.email, params.password).validate(() async {
outputPort.loading();
Result<bool> result = await _repository.login(params);
result.when(
success: (data) {
outputPort.success();
},
error: (error) {
outputPort.requestError(error);
},
);
}, (errors) {
outputPort.formValidationErrors(errors);
});
}
}
and finally I handle the presentation logic in the presenter which it implements the output port of the login use case
class LoginPresenter implements LoginUseCaseOutputPort {
Reader read;
LoginPresenter(this.read) : super(LoginState.initial());
@override
void formValidationErrors(List<Failure> errors) =>
state = LoginState.formValidationErrors(errors);
@override
void success() => read(setRootPresenterProvider.notifier).setMainPageAsRoot();
@override
void loading() => state = LoginState.loading();
@override
void requestError(Failure error) => state = LoginState.requestError(error);
}
What I do I create a failure class for each input field and return failures of all fields in a list and in the presentation logic I check the input failures by type
My Question: What if I have a large form (eg:15 fields), Should I create a failure class for each of them? Is there a better way to handle the validation?
CodePudding user response:
My Question: What if I have a large form (eg:15 fields), Should I create a failure class for each of them? Is there a better way to handle the validation?
Every error that can occur must be identifiable. Either through dedicated classes, failure codes or constants. Which one you choose depends on the error context information that you want to provide to clients.
I also wouldn't put messages in the failure object, because it is up to a presenter to choose a presentation for a error type. The way an error is presented to the user highly depends on the user interface. Maybe an error doesn't need a string, it is just presented as an icon or a colored marker and so on. The interactor should not create language specific strings. This is part of the user interface.
In simple cases you might only need an error code or constant like 401
and the presenter converts it to Login failed
. Of course you can use a common error object for this cases too.
class Failure { int code; }
In other cases you might want to display a more detailed error message like E-mails under the domain '@somedomain.com' are not allowed to login.
. In this cases you should use an error object instead of only a simple code to provide details. E.g.
class LoginDomainFailure { String localPart; String domainName }
A presenter can then use this information to generate a failure string or present the error in some other way, e.g. highlighting the domain name in the input field or whatever you can imagine.