Прямо сейчас в OTUS открыт набор на новый поток курса «Android Developer. Basic». В преддверии старта курса традиционно подготовили для вас интересный перевод, а так же предлагаем посмотреть день открытых дверей по курсу, в рамках которого вы подробно узнаете о процессе обучение и получите ответы на интересующие вопросы.
Удобный способ валидации форм
«Чтобы научиться чему-то хорошо, нужно научиться делать это несколькими способами».
Несколько дней назад я работал над проектом, где мне нужно было реализовать валидацию элементов формы textInputLayout и textInputEditText с помощью связывания данных. К сожалению, доступно не так много документации на эту тему.
В конце концов я добился желаемого результата, изучив кое-какие материалы и проведя ряд экспериментов. Вот что я хотел получить:


Уверен, что многие разработчики хотели бы реализовать такой же функционал и удобное взаимодействие с формами. Итак, давайте начнем.
Что нам потребуется?
Я разобью проект на этапы, чтобы легче было понять, что мы делаем.
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/
Добавить комментарий