Эта статья - обзор моей Android-библиотеки валидации, которая позволяет организовать сложную валидацию данных. В первую очередь библиотека рассчитана на проверку пользовательского ввода. Часто в мобильных приложениях приходится делать различные экраны для ввода пользователем информации. Но так как пользователи не отличаются умом и сообразительностью - приходится проверять, что они там написали: запрещенные символы, максимальная длина, соответствие RegExp и так далее. Первое, что приходит в голову для решения проблемы - прицепить регулярку прямо на EditText  Например, вот так: Но а если теперь я хочу проверять поле по двум разным RegExp и выводить разные ошибки? Ну тогда можно добавить второй слушатель: Однако, теперь если в поле ввести строку "12345" то ошибки не будет: первый слушатель выставит ошибку на поле, потому что поле содержит цифры, а вот второй слушатель скроет ошибку, потому что по его мнению - поле правильное, не более 10 символов. И это только одна проблема. Дальше - веселее: Как повесить на поле множество правил валидации, чтобы они правильно работали вместе? Как добавлять или удалять правила? Где хранить эту кучу валидаторов? Как проверять любые типы данных, а не только строковые? и многое другое... Вся валидация должна происходить во ViewModel или Presenter, но не в UI слое. Задача UI - должным образом реагировать на результат проверки. Не должно быть никаких специальных view-классов с поддержкой валидации. Валидацию можно привязать к любой view Валидаторы должны поддерживать множество различных правил проверки, чтобы, например, каждое правило выдавало собственную ошибку. Схематично всё выглядит примерно вот так: Всё это я реализовал в отдельной библиотеке. Теперь рассмотрим её подробнее. В основе всей валидации лежит условие (Condition) - простейший интерфейс с одним методом validate. На изи! У Condition есть метод validate(data), который проверит данные и вернёт результат ValidationResult. Внутри ValidationResult будет булевый результат проверки isValid и сообщение об ошибке, которое должно появляться если isValid == false Condition можно складывать и умножать. Сложение работает как аналог булевого ИЛИ, а умножение как аналог булевого И Сложение (ИЛИ) ИЛИ Conditon(true) Conditon(false) Conditon(true) Conditon(true) Conditon(true) Conditon(false) Conditon(true) Conditon(false) Умножение (И) И Conditon(true) Conditon(false) Conditon(true) Conditon(true) Conditon(false) Conditon(false) Conditon(false) Conditon(false) Точно так же склыдываются или умножаются ValidationResult А что если надо проверять значение по множеству условий? Validator по-сути является Condition, только более прокаченный. Внутри Validator  находится множуство условий Set<Condition>. В момент проверки значение проверяется по каждому из условий, формируется набор результатов валидации Set<ValidationResult>. Затем, этот набор с результатами передается на вход оператору (Operator), который и решает, какой будет финальный результат валидации. Вот, Всё. У валидатора есть свои приколы: Operator - это просто Condition<Collection<ValidationResult>>, то есть тупа проверяет коллекцию результатов валидации. Получается такой аналог логического оператора из начального курса булевой алгебры. По-умолчанию используется оператор-конъюнкция. Но можно написать свой оператор, который, например, будет выдавать ValidationResult(true) если количество валидных условий достигло порогового значения. Установка оператора Удаление оператора Нельзя удалять оператор! Validator не может работать без оператора Наблюдение за изменением оператора Может быть такое, что необходимо отслеживать изменения оператора. Например, чтобы обновить view. Добавление условия Удаление условия Наблюдение за изменением условий Чтобы следить за списком условий - добавьте слушателя OnConditionsChangedListener, который будет вызываться при любом изменении условий Изменение условий Если нужно сделать много преобразований можно использовать changeConditionsSet, чтобы слушатель OnConditionsChangedListener сработал только один раз - после всех преобразований набора условий. Было бы удобно, если бы валидатор самостоятельно проверял данные при каждом их изменении. Так и сделаем! Сейчас модно молодежно использовать LiveData. Так пусть валидатор подпишется на неё и будет проверять каждое значение.  LiveDataValidator работает так же как и обычный Vlidator, однако у него есть свои особенности: Состояние это результат последней проверки.  Представляет собой LiveData<ValidationResult>, поэтому за состоянием валидатора можно удобно следить. LiveDataValidator всегда в актуальном состоянии пока он подписан на источник (Validator.observe; Validator.observeForever) LiveDataValidator начинает работать только тогда, когда хоть кто-нибудь подписан на него LiveDataValidator умеет следить за другими LiveData и реагировать на их изменения Для этого есть метод watchOn В примере выше liveDataValidator следит за полем textMaxLength и как только значение  textMaxLength меняется liveDataValidator принудительно валидируется Для подобных случаев есть метод triggerOn, который запускает валидацию всякий раз когда изменяется дополнительный источник Пример Есть 2 текстовых поля: на одном пики точены, на другом х** д*ы вовсе не пики Задача, чтобы второе поле не содержало в себе текст первого поля Как видно, secondValidator проверяет поле second, но при этом использует исползует first для проверки. Но что если first изменился? Тогда валидатор будет висеть в неактуальном состоянии до следующего изменения second. Поэтому валидатору нужно следить за first и при каждом его изменении принудительно выполнять проверку Делается это методом triggerOn(LiveData<*>), который будет запускать валидатор при каждом изменении first Вместо triggerOn можно так же использовать watchOn и самостоятельно прописать нужное действие А теперь, когда у нас есть куча полей с LiveDataValidator'ами надо каким-то образом опредилить общий результат валидации. Самый распространённый пример: если все поля на форме заполнены правильно - включаем кнопку "Далее". Для этого есть MuxLiveDataValidator.  Он подписывается на множество LiveDataValidator'ов и как только один из них изменяется - MuxLiveDataValidator собирает состояния (ValidationResult) всех LiveDataValidator'ов и отдаёт их на проверку оператору (Operator). Operator выдаёт окончательный результат. Короче, MuxLiveDataValidator работает типа как мультиплексор. Отсюда и название. Аналогично LiveDataValidator у MuxLiveDataValidator  есть состояние Состояние это LiveData<ValidationResult> в котором находится последний результат проверки. Тут как у LiveDataValidator - доступ только по подписке Примечание Когда вы подписываетесь на MuxLiveDataValidator, то все его LiveDataValidator активируются, то есть подписка распространяется и на них (такой вот аналог семейной подписки у MediatorLiveData ). То есть если вы подписались на MuxLiveDataValidator, то не можно не подписываться на те LiveDataValidator, за которыми он следит. Добавить LiveDataValidator можно при создании MuxLiveDataValidator Можно и после создания Ну тут типа ваще всё изян По-умолчанию MuxLiveDataValidator использует оператор-конъюнкцию. Чтобы поменять логику выдачи финального ValidationResult нужно установить другой оператор Есть возможность следить за сменой оператора чтобы, например, очистить ошибку на view. ConditionViewBinder базовый связыватель view и Condition Работает так: В момент вызова ConditionViewBinder.validate() достает из view данные для проверки абстрактным методом getValidationData(). Эти данные улетают в Condition, который проверит их и вернет ValidationResult. Затем этот ValidationResult передаётся абстрактному методу onValidationResult() в котором и происходит изменения view. Пример Таким образом можно привязать любой валидатор к любой view Предназначен для более удобной работы с Validator: следит за изменениями оператора и условий валидатора. LiveDataValidator - особый пациент. Для него свой binder, который: сам подписывается/отписывается на LiveDataValidator ( чтобы активировать его) getValidationData()  берется не из view, а прямо из валидатора (из его source) Активация LiveDataValidatorViewBinder нужно активировать. Тут 2 способа: Через конструктор. В конструктор передать LifeycleOwner Просто вызвать attach TextConditionViewBinder Связывает простые Condition с TextView. Проверяет поле при каждом изменении текста в нём Использовать так: TextViewLiveDataValidatorBinder Тут то же самое, что и TextConditionViewBinder, но тут работаем с LiveDataValidator. Использовать так: Во ViewModel делаем простейший Condition Во фрагменте (или активити) применяем условие к текстовому полю Готово Допустим у нас есть 3 поля: поле для ввода цифр, поле для ввода букв и поле, которое указывает максимальную длину поля ввода цифр. О как! А ещё нужно выводить общее состояние валидации всей формы в отдельное текстовое поле! Пример доступен тут: https://github.com/Egor-Blagochinnov/ValidationSample Для начала объявим сами поля и валидаторы к ним во ViewModel] Чтобы динамически менять условия валидации - лучше всего написать свой валидатор. Потому что для смены условий нужно хранить ссылки на эти самые условия, а это лучше сделать в отдельном классе Теперь идём во фрагмент и подключаем всё это дело Готово! Все валидаторы должны находиться во ViewModel (ну или в Presenter) Не надо выносить логику валидирования во фрагменты, активности и вообще на view уровень.  По-возможности используйте LiveDataValidator. Он самый прокаченный. И вообще вся библиотека ради него написана была Аккуратнее с множеством условий.  Вы можете добавить на поле противоречащие друг другу условия и будет непонятно что! Делайте свои реализации. Создавайте свои ConditionViewBinderы, чтобы работать с кастомными view Создавайте свои валидаторы если вам нужна более сложная валидация