Прямо сейчас в 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
Надеюсь, вы узнали из этой статьи что-то новое для себя. Следите за появлением новых статей! Успехов в разработке!
MaksimNovikov
Впринципе не плохая статья.
Пара замечаний:
1) в одном файле разный стиль, где то when где то каскад if else
2) вынести рессивером binding отчистить код