В процессе профессиональной деятельности приходится постоянно сталкиваться с валидацией полей ввода текста на экранах мобильных устройств. Чаще всего это пара-тройка экранов на приложение (SignIn, SignUp, Profile).
Ради этого подтягивать внешние зависимости представляется избыточным. Например, тот же Hibernate Validator добавляет порядка 8000 методов и 1 мб к весу финальной apk, что выглядит… избыточным )
Поиск какого-то удачного решения в интернетах не увенчался успехом, поэтому было принято решение напилить свое. Посовещавшись с коллегой (с iOS направления) пришли к идее лаконичного решения, которая была впоследствии реализована на каждой из платформ.
Реализацией этой идеи под Android и хочу поделиться. Но предупрежу сразу, что в угоду простоте восприятия идеи примеры использования будут приводится в крайне простом варианте, возможно даже наивном. Плюс, в текущей реализации идеи не используется Rx.
Итак. Начинается все с очень простого интерфейса:
Собственно, это все )
Дальше нам понадобится его реализация для компоновки однотипных атомарных валидаторов воедино:
В качестве примера можно привести валидацию email поля. Для этого создадим два атомарных валидатора:
И, собственно, вариант использования:
Также эта идея подходит и для валидации DataObject’ов с последующим выводом ошибки на экран. Например, создадим валидатор для DataObject’а User:
И вариант использования:
Варианты валидации могут быть самыми различными: от валидации по ивенту (по нажатию кнопки, например, как это показано в примерах выше), до динамической валидации (в момент ввода текста):
Равно как и стратегия показа ошибок тоже может совершенно различной: от фокуса на поле ошибки с показом текста ошибки и показом клавиатуры (предпочтительный на мой взгляд), до показа всех ошибок на экране разом (довольно часто встречается в андроид приложениях именно такой вариант). Рекомендации Google по показу ошибок можно посмотреть по ссылке.
По итогу получаем крайне простую и вместе с тем гибкую идею валидации полей и объектов данных, которая по-сути ни капли не утяжеляет проект и реализуется по щелчку пальцев. Как бэ профит )
Ради этого подтягивать внешние зависимости представляется избыточным. Например, тот же Hibernate Validator добавляет порядка 8000 методов и 1 мб к весу финальной apk, что выглядит… избыточным )
Поиск какого-то удачного решения в интернетах не увенчался успехом, поэтому было принято решение напилить свое. Посовещавшись с коллегой (с iOS направления) пришли к идее лаконичного решения, которая была впоследствии реализована на каждой из платформ.
Реализацией этой идеи под Android и хочу поделиться. Но предупрежу сразу, что в угоду простоте восприятия идеи примеры использования будут приводится в крайне простом варианте, возможно даже наивном. Плюс, в текущей реализации идеи не используется Rx.
Итак. Начинается все с очень простого интерфейса:
public interface Validator<T> {
boolean isValid(T value);
String getDescription();
}
Собственно, это все )
Дальше нам понадобится его реализация для компоновки однотипных атомарных валидаторов воедино:
public class ValidatorsComposer<T> implements Validator<T> {
private final List<Validator<T>> validators;
private String description;
public ValidatorsComposer(Validator<T>... validators) {
this.validators = Arrays.asList(validators);
}
@Override
public boolean isValid(T value) {
for (Validator<T> validator : validators) {
if (!validator.isValid(value)) {
description = validator.getDescription();
return false;
}
}
return true;
}
@Override
public String getDescription() {
return description;
}
}
В качестве примера можно привести валидацию email поля. Для этого создадим два атомарных валидатора:
public class EmptyValidator implements Validator<String> {
@Override
public boolean isValid(String value) {
return !TextUtils.isEmpty(value);
}
@Override
public String getDescription() {
return "Field must not be empty";
}
}
public class EmailValidator implements Validator<String> {
@Override
public boolean isValid(String value) {
return Patterns.EMAIL_ADDRESS.matcher(value).matches();
}
@Override
public String getDescription() {
return "Email should be in \'a@a.com\' format";
}
}
И, собственно, вариант использования:
final ValidatorsComposer<String> emailValidatorsComposer =
new ValidatorsComposer<>(new EmptyValidator(), new EmailValidator());
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (emailValidatorsComposer.isValid(emailEditText.getText().toString())) {
errorTextView.setText(null);
} else {
errorTextView.setText(emailValidatorsComposer.getDescription());
}
}
});
Также эта идея подходит и для валидации DataObject’ов с последующим выводом ошибки на экран. Например, создадим валидатор для DataObject’а User:
public class User {
public final String name;
public final Integer age;
public final Gender gender;
public enum Gender {MALE, FEMALE}
public User(String name, Integer age, Gender gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
}
public class UserValidator implements Validator<User> {
private String description;
@Override
public boolean isValid(User value) {
if (value == null) {
description = "User must not be null";
return false;
}
final String name = value.name;
if (TextUtils.isEmpty(name)) {
description = "User name must not be blank";
return false;
}
final Integer age = value.age;
if (age == null) {
description = "User age must not be blank";
return false;
} else if (age < 0) {
description = "User age must be above zero";
return false;
} else if (age > 100) {
description = "User age is to much";
return false;
}
final User.Gender gender = value.gender;
if (gender == null) {
description = "User gender must not be blank";
return false;
}
return true;
}
@Override
public String getDescription() {
return description;
}
}
И вариант использования:
final Validator<User> userValidator = new UserValidator();
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
User user = new User(null, 12, User.Gender.MALE);
if (userValidator.isValid(user)) {
errorTextView.setText(null);
} else {
errorTextView.setText(userValidator.getDescription());
}
}
});
Варианты валидации могут быть самыми различными: от валидации по ивенту (по нажатию кнопки, например, как это показано в примерах выше), до динамической валидации (в момент ввода текста):
emailEditText.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (!emailValidatorsComposer.isValid(s.toString())) {
errorTextView.setText(emailValidatorsComposer.getDescription());
}
}
});
Равно как и стратегия показа ошибок тоже может совершенно различной: от фокуса на поле ошибки с показом текста ошибки и показом клавиатуры (предпочтительный на мой взгляд), до показа всех ошибок на экране разом (довольно часто встречается в андроид приложениях именно такой вариант). Рекомендации Google по показу ошибок можно посмотреть по ссылке.
По итогу получаем крайне простую и вместе с тем гибкую идею валидации полей и объектов данных, которая по-сути ни капли не утяжеляет проект и реализуется по щелчку пальцев. Как бэ профит )
Поделиться с друзьями
Комментарии (2)
Dwite
10.07.2017 08:16Может вместо
public String getDescription() {}
использовать какой-нибудь > getErrorMessage. Исходя из своего опыта думаю что было бы хорошо еще расширить
boolean isValid(T value);
Проверяя по набору каких-нибудь Predicate и текст ошибки был привязан к Predicate, а не один текст для всех видов ошибок. Таким образом создавая Validator из набора Predicate(ов) можно и текст ошибки сделать локализированным и сам валидор будет более гибким.
AnneSmith
если вы используете не класс валидатора, а класс сообщения, у которого методом являтеся простая логика, соответствующая этому сообщению — " return age < 0" — возвращающая true/false, то ваш код станет коротким циклом по множеству объектов класса сообщений, сообщения можно будет ассоциировать только с полями, где они должны быть показаны, а не с целым юзером, сам код станет re-usable, сообщения и логику можно легко заменять/конфигурировать и тоже использовать в следующем проекте или даже в общей библиотеке валидаций и не только для сообщений об ошибках
причем в вашем варианте, скорее всего каждое следущее сообщение будет переписывать предыдущее, если условие выполняется, а класс сообщений может иметь дополнительный флаг, чтобы не проводить последующую валидацию, пока не ушла первая ошибка в этом же поле
идеально, конечно, использовать еще более абстрактный класс типа object/widget, но для небольших форм может быть достаточно класса «Message»