Валидация элементов формы textInputLayout в Android с помощью связывания данных

от автора

Прямо сейчас в OTUS открыт набор на новый поток курса «Android Developer. Basic». В преддверии старта курса традиционно подготовили для вас интересный перевод, а так же предлагаем посмотреть день открытых дверей по курсу, в рамках которого вы подробно узнаете о процессе обучение и получите ответы на интересующие вопросы.


Удобный способ валидации форм

«Чтобы научиться чему-то хорошо, нужно научиться делать это несколькими способами».

Несколько дней назад я работал над проектом, где мне нужно было реализовать валидацию элементов формы textInputLayout и textInputEditText с помощью связывания данных. К сожалению, доступно не так много документации на эту тему.

В конце концов я добился желаемого результата, изучив кое-какие материалы и проведя ряд экспериментов. Вот что я хотел получить:

 

Финальный вид приложения
Финальный вид приложения

Уверен, что многие разработчики хотели бы реализовать такой же функционал и удобное взаимодействие с формами. Итак, давайте начнем.

Что нам потребуется?

  1. Kotlin

  2. Статья Связывание данных

  3. Библиотека Material

Я разобью проект на этапы, чтобы легче было понять, что мы делаем.

1. Настроим исходный проект и включим связывание данных в файле build.gradle(:app), добавив под тег android{} следующую строку:

dataBinding{         enabled true }

Для использования элементов textInputLayout и textInputEditText необходимо включить поддержку Material для Android, добавив в файл build.gradle(:app) следующую зависимость:

implementation 'com.google.android.material:material:1.2.1'

Создадим макет нашей формы. Я сделаю простой макет, потому что моя цель — определить его основной функционал, а не создать хороший дизайн.

Я создал вот такой простой макет:

 

Вот содержимое файла activity_main.xml:

<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     xmlns:tools="http://schemas.android.com/tools">      <LinearLayout         android:layout_width="match_parent"         android:layout_height="match_parent"         android:layout_margin="10dp"         android:orientation="vertical"         tools:context=".MainActivity">           <com.google.android.material.textfield.TextInputLayout             android:id="@+id/userNameTextInputLayout"             style="@style/TextInputLayoutBoxColor"             android:layout_width="match_parent"             android:layout_height="wrap_content"             android:layout_marginTop="10dp"             android:hint="@string/username"             app:endIconMode="clear_text"             app:errorEnabled="true"             app:hintTextAppearance="@style/TextAppearance.App.TextInputLayout">              <com.google.android.material.textfield.TextInputEditText                 android:id="@+id/userName"                 android:layout_width="match_parent"                 android:layout_height="50dp"                 android:inputType="textPersonName" />           </com.google.android.material.textfield.TextInputLayout>          <com.google.android.material.textfield.TextInputLayout             android:id="@+id/emailTextInputLayout"             style="@style/TextInputLayoutBoxColor"             android:layout_width="match_parent"             android:layout_height="wrap_content"             android:layout_marginTop="5dp"             android:hint="@string/email"             app:endIconMode="clear_text"             app:errorEnabled="true"             app:hintTextAppearance="@style/TextAppearance.App.TextInputLayout">              <com.google.android.material.textfield.TextInputEditText                 android:id="@+id/email"                 android:layout_width="match_parent"                 android:layout_height="50dp"                 android:inputType="textEmailAddress" />           </com.google.android.material.textfield.TextInputLayout>          <com.google.android.material.textfield.TextInputLayout             android:id="@+id/passwordTextInputLayout"             style="@style/TextInputLayoutBoxColor"             android:layout_width="match_parent"             android:layout_height="wrap_content"             android:layout_marginTop="5dp"             android:hint="@string/password"             app:errorEnabled="true"             app:hintTextAppearance="@style/TextAppearance.App.TextInputLayout"             app:passwordToggleEnabled="true">              <com.google.android.material.textfield.TextInputEditText                 android:id="@+id/password"                 android:layout_width="match_parent"                 android:layout_height="50dp"                 android:inputType="textPassword" />           </com.google.android.material.textfield.TextInputLayout>          <com.google.android.material.textfield.TextInputLayout             android:id="@+id/confirmPasswordTextInputLayout"             style="@style/TextInputLayoutBoxColor"             android:layout_width="match_parent"             android:layout_height="wrap_content"             android:layout_marginTop="5dp"             android:hint="@string/confirm_password"             app:errorEnabled="true"             app:hintTextAppearance="@style/TextAppearance.App.TextInputLayout"             app:passwordToggleEnabled="true">              <com.google.android.material.textfield.TextInputEditText                 android:id="@+id/confirmPassword"                 android:layout_width="match_parent"                 android:layout_height="50dp"                 android:inputType="textPassword"                 app:passwordToggleEnabled="true" />           </com.google.android.material.textfield.TextInputLayout>          <com.google.android.material.button.MaterialButton             android:id="@+id/loginButton"             android:layout_width="match_parent"             android:layout_height="wrap_content"             android:text="@string/login"             android:textAllCaps="false" />      </LinearLayout> </layout>

Если вас смущают теги <layout>, не переживайте — о них я написал в своей предыдущей статье.

Наш макет готов. Теперь займемся кодом.

2. На GIF-анимации, показывающей поведение финального варианта приложения (см. выше), видно, как появляются и исчезают сообщения об ошибках, когда заданные условия принимают значение true. Это происходит потому, что я связал каждое текстовое поле с объектом TextWatcher, к которому постоянно происходит обращение по мере ввода текста пользователем.

В файле MainActivity.kt я создал класс, который унаследован от класса TextWatcher:

/**      * applying text watcher on each text field      */     inner class TextFieldValidation(private val view: View) : TextWatcher {         override fun afterTextChanged(s: Editable?) {}         override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}         override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {             // checking ids of each text field and applying functions accordingly.          }      }

Параметр view, который передается в конструктор класса, я опишу позже.

3. Это основная часть. У каждого текстового поля имеется ряд условий, которые должны иметь значение true перед отправкой данных формы. Код, задающий условия для каждого текстового поля, представлен ниже:

  /**      * field must not be empy      */     private fun validateUserName(): Boolean {         if (binding.userName.text.toString().trim().isEmpty()) {             binding.userNameTextInputLayout.error = "Required Field!"             binding.userName.requestFocus()             return false         } else {             binding.userNameTextInputLayout.isErrorEnabled = false         }         return true     }      /**      * 1) field must not be empty      * 2) text should matches email address format      */     private fun validateEmail(): Boolean {         if (binding.email.text.toString().trim().isEmpty()) {             binding.emailTextInputLayout.error = "Required Field!"             binding.email.requestFocus()             return false         } else if (!isValidEmail(binding.email.text.toString())) {             binding.emailTextInputLayout.error = "Invalid Email!"             binding.email.requestFocus()             return false         } else {             binding.emailTextInputLayout.isErrorEnabled = false         }         return true     }      /**      * 1) field must not be empty      * 2) password lenght must not be less than 6      * 3) password must contain at least one digit      * 4) password must contain atleast one upper and one lower case letter      * 5) password must contain atleast one special character.      */     private fun validatePassword(): Boolean {         if (binding.password.text.toString().trim().isEmpty()) {             binding.passwordTextInputLayout.error = "Required Field!"             binding.password.requestFocus()             return false         } else if (binding.password.text.toString().length < 6) {             binding.passwordTextInputLayout.error = "password can't be less than 6"             binding.password.requestFocus()             return false         } else if (!isStringContainNumber(binding.password.text.toString())) {             binding.passwordTextInputLayout.error = "Required at least 1 digit"             binding.password.requestFocus()             return false         } else if (!isStringLowerAndUpperCase(binding.password.text.toString())) {             binding.passwordTextInputLayout.error =                 "Password must contain upper and lower case letters"             binding.password.requestFocus()             return false         } else if (!isStringContainSpecialCharacter(binding.password.text.toString())) {             binding.passwordTextInputLayout.error = "1 special character required"             binding.password.requestFocus()             return false         } else {             binding.passwordTextInputLayout.isErrorEnabled = false         }         return true     }      /**      * 1) field must not be empty      * 2) password and confirm password should be same      */     private fun validateConfirmPassword(): Boolean {         when {             binding.confirmPassword.text.toString().trim().isEmpty() -> {                 binding.confirmPasswordTextInputLayout.error = "Required Field!"                 binding.confirmPassword.requestFocus()                 return false             }             binding.confirmPassword.text.toString() != binding.password.text.toString() -> {                 binding.confirmPasswordTextInputLayout.error = "Passwords don't match"                 binding.confirmPassword.requestFocus()                 return false             }             else -> {                 binding.confirmPasswordTextInputLayout.isErrorEnabled = false             }         }         return true     }

4. Теперь необходимо связать каждое текстовое поле с классом textWatcher, который был создан ранее:

 private fun setupListeners() {         binding.userName.addTextChangedListener(TextFieldValidation(binding.userName))         binding.email.addTextChangedListener(TextFieldValidation(binding.email))         binding.password.addTextChangedListener(TextFieldValidation(binding.password))         binding.confirmPassword.addTextChangedListener(TextFieldValidation(binding.confirmPassword))     }

Но как класс TextFieldValidation узнает, с каким текстовым полем нужно связываться? Прокрутив статью выше, вы увидите, что я добавил следующий комментарий в один из методов класса TextFieldValidation:

// проверка идентификаторов текстовых полей и применение соответствующих функций

Обратите внимание, что я передаю параметр view в конструктор класса TextFieldValidation, который отвечает за разделение каждого текстового поля и применение каждого из указанных выше методов следующим образом:

/**      * applying text watcher on each text field      */     inner class TextFieldValidation(private val view: View) : TextWatcher {         override fun afterTextChanged(s: Editable?) {}         override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}         override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {             // checking ids of each text field and applying functions accordingly.             when (view.id) {                 R.id.userName -> {                     validateUserName()                 }                 R.id.email -> {                     validateEmail()                 }                 R.id.password -> {                     validatePassword()                 }                 R.id.confirmPassword -> {                     validateConfirmPassword()                 }             }         }     }

Финальный вариант файла MainActivity.kt выглядит так:

package com.example.textinputlayoutformvalidation  import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.util.Patterns import android.view.View import android.widget.Toast import androidx.databinding.DataBindingUtil import com.example.textinputlayoutformvalidation.FieldValidators.isStringContainNumber import com.example.textinputlayoutformvalidation.FieldValidators.isStringContainSpecialCharacter import com.example.textinputlayoutformvalidation.FieldValidators.isStringLowerAndUpperCase import com.example.textinputlayoutformvalidation.FieldValidators.isValidEmail import com.example.textinputlayoutformvalidation.databinding.ActivityMainBinding  /**  * created by : Mustufa Ansari  * Email : mustufaayub82@gmail.com  */  class MainActivity : AppCompatActivity() {      lateinit var binding: ActivityMainBinding       override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         binding = DataBindingUtil.setContentView(this, R.layout.activity_main)          setupListeners()          binding.loginButton.setOnClickListener {             if (isValidate()) {                 Toast.makeText(this, "validated", Toast.LENGTH_SHORT).show()             }         }     }      private fun isValidate(): Boolean =         validateUserName() && validateEmail() && validatePassword() && validateConfirmPassword()      private fun setupListeners() {         binding.userName.addTextChangedListener(TextFieldValidation(binding.userName))         binding.email.addTextChangedListener(TextFieldValidation(binding.email))         binding.password.addTextChangedListener(TextFieldValidation(binding.password))         binding.confirmPassword.addTextChangedListener(TextFieldValidation(binding.confirmPassword))     }      /**      * field must not be empy      */     private fun validateUserName(): Boolean {         if (binding.userName.text.toString().trim().isEmpty()) {             binding.userNameTextInputLayout.error = "Required Field!"             binding.userName.requestFocus()             return false         } else {             binding.userNameTextInputLayout.isErrorEnabled = false         }         return true     }      /**      * 1) field must not be empty      * 2) text should matches email address format      */     private fun validateEmail(): Boolean {         if (binding.email.text.toString().trim().isEmpty()) {             binding.emailTextInputLayout.error = "Required Field!"             binding.email.requestFocus()             return false         } else if (!isValidEmail(binding.email.text.toString())) {             binding.emailTextInputLayout.error = "Invalid Email!"             binding.email.requestFocus()             return false         } else {             binding.emailTextInputLayout.isErrorEnabled = false         }         return true     }      /**      * 1) field must not be empty      * 2) password lenght must not be less than 6      * 3) password must contain at least one digit      * 4) password must contain atleast one upper and one lower case letter      * 5) password must contain atleast one special character.      */     private fun validatePassword(): Boolean {         if (binding.password.text.toString().trim().isEmpty()) {             binding.passwordTextInputLayout.error = "Required Field!"             binding.password.requestFocus()             return false         } else if (binding.password.text.toString().length < 6) {             binding.passwordTextInputLayout.error = "password can't be less than 6"             binding.password.requestFocus()             return false         } else if (!isStringContainNumber(binding.password.text.toString())) {             binding.passwordTextInputLayout.error = "Required at least 1 digit"             binding.password.requestFocus()             return false         } else if (!isStringLowerAndUpperCase(binding.password.text.toString())) {             binding.passwordTextInputLayout.error =                 "Password must contain upper and lower case letters"             binding.password.requestFocus()             return false         } else if (!isStringContainSpecialCharacter(binding.password.text.toString())) {             binding.passwordTextInputLayout.error = "1 special character required"             binding.password.requestFocus()             return false         } else {             binding.passwordTextInputLayout.isErrorEnabled = false         }         return true     }      /**      * 1) field must not be empty      * 2) password and confirm password should be same      */     private fun validateConfirmPassword(): Boolean {         when {             binding.confirmPassword.text.toString().trim().isEmpty() -> {                 binding.confirmPasswordTextInputLayout.error = "Required Field!"                 binding.confirmPassword.requestFocus()                 return false             }             binding.confirmPassword.text.toString() != binding.password.text.toString() -> {                 binding.confirmPasswordTextInputLayout.error = "Passwords don't match"                 binding.confirmPassword.requestFocus()                 return false             }             else -> {                 binding.confirmPasswordTextInputLayout.isErrorEnabled = false             }         }         return true     }      /**      * applying text watcher on each text field      */     inner class TextFieldValidation(private val view: View) : TextWatcher {         override fun afterTextChanged(s: Editable?) {}         override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}         override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {             // checking ids of each text field and applying functions accordingly.             when (view.id) {                 R.id.userName -> {                     validateUserName()                 }                 R.id.email -> {                     validateEmail()                 }                 R.id.password -> {                     validatePassword()                 }                 R.id.confirmPassword -> {                     validateConfirmPassword()                 }             }         }     } }

Запустим приложение и полюбуемся поведением формы ввода:

 

Полный исходный код этого проекта можно скачать по ссылке ниже:

https://github.com/Mustufa786/TextInputLayout-FormValidation

Надеюсь, вы узнали из этой статьи что-то новое для себя. Следите за появлением новых статей! Успехов в разработке!

Узнать подробнее о курсе.

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/529886/