Plantilla para crear un formulario funcional, petición de datos y con validación de campos en tiempo de escritura.

En esta plantilla se incluye lo siguiente:
- LinearLayout
- TextInputLayout
- TextInputEditText
- AppCompatButton
- ValidatorFieldHelper
- ValidationMainFields
- Extensiones Kotlin
Contenedor
Layout content_main.xml conformado con
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_main"
tools:context=".MainActivity">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/validate_email"
android:imeOptions="actionNext"
android:inputType="textEmailAddress"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtUserName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:ellipsize="end"
android:hint="@string/validate_username"
android:imeOptions="actionNext"
android:inputType="textPersonName"
android:paddingEnd="50dp"
android:singleLine="true">
</com.google.android.material.textfield.TextInputEditText>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/btnAutoUsername"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_alignParentEnd="true"
android:layout_marginEnd="2dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="10dp"
app:srcCompat="@drawable/ic_gavel_black_24dp" />
</RelativeLayout>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/validate_password"
android:imeOptions="actionNext"
android:inputType="textPassword"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edtCofirmPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/validate_password_confirm"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnValidate"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:focusable="true"
android:text="@string/validate_btn" />
</LinearLayout>
ValidatorFieldHelper
ValidatorFieldHelper.kt Ayuda para la verificación de campos
abstract class ValidatorFieldHelper {
interface ValidatorFieldsListener {
fun onSuccessfulValidator()
fun onErrorValidator(assertionList: HashMap<String, AssertionItem>)
}
data class AssertionItem(val isValid: Boolean, val error: String? = null)
private var listener: ValidatorFieldsListener? = null
open var isValid: Boolean = false
private val assertionList: HashMap<String, AssertionItem> = hashMapOf()
//Append new field
fun addAssertion(key: String) {
assertionList[key] = AssertionItem(false)
}
//Get a validation field
fun getAssertion(key: String): AssertionItem? {
return assertionList[key]
}
//Update a validation field
fun setAssertion(key: String, value: AssertionItem) {
assertionList[key] = value
}
//Run validate all fields
fun validate() {
isValid = false
if (assertionList.size == 0) return
isValid = true
assertionList.forEach {
if (!it.value.isValid) isValid = false
}
if (isValid) listener?.onSuccessfulValidator() else listener?.onErrorValidator(assertionList)
}
fun setOnValidateListener(listener: ValidatorFieldsListener) {
this.listener = listener
}
}
ValidatorMainFields
ValidationMainFields.kt validar los campos del formulario
class ValidationMainFields(private val context: Context) : ValidatorFieldHelper() {
init {
addAssertion(FIELD_EMAIL)
addAssertion(FIELD_USER_NAME)
addAssertion(FIELD_PASSWORD)
addAssertion(FIELD_CONFIRM_PASSWORD)
}
fun checkFieldEmail(s: String): AssertionItem? {
var isValid = false
var errorMsg: String? = null
if (s.isNotEmpty()) {
if (isValidEmail(s)) {
isValid = true
} else {
errorMsg = context.getString(R.string.error_email)
}
} else {
errorMsg = context.getString(R.string.error_empty)
}
setAssertion(FIELD_EMAIL, AssertionItem(isValid, errorMsg))
return getAssertion(FIELD_EMAIL)
}
fun checkFieldUserName(s: String): AssertionItem? {
var isValid = false
var errorMsg: String? = null
if (s.isNotEmpty()) {
if (isUniqueUserName(s)) {
isValid = true
} else {
errorMsg = context.getString(R.string.error_username)
}
} else {
errorMsg = context.getString(R.string.error_empty)
}
setAssertion(FIELD_USER_NAME, AssertionItem(isValid, errorMsg))
return getAssertion(FIELD_USER_NAME)
}
fun checkFieldPassword(s: String): AssertionItem? {
var isValid = false
var errorMsg: String? = null
if (s.isNotEmpty()) {
isValid = true
} else {
errorMsg = context.getString(R.string.error_empty)
}
setAssertion(FIELD_PASSWORD, AssertionItem(isValid, errorMsg))
return getAssertion(FIELD_PASSWORD)
}
fun checkFieldConfirmPassword(password1: String, password2: String): AssertionItem? {
var isValid = false
var errorMsg: String? = null
if (password2.isNotEmpty()) {
isValid = password1 == password2
if (!isValid) errorMsg = context.getString(R.string.error_password_confirm)
} else {
errorMsg = context.getString(R.string.error_empty)
}
setAssertion(FIELD_CONFIRM_PASSWORD, AssertionItem(isValid, errorMsg))
return getAssertion(FIELD_CONFIRM_PASSWORD)
}
private fun isValidEmail(target: CharSequence): Boolean {
return if (TextUtils.isEmpty(target)) {
false
} else {
Patterns.EMAIL_ADDRESS.matcher(target).matches()
}
}
private fun isUniqueUserName(s: String): Boolean {
return s != "bob"
}
companion object {
const val FIELD_EMAIL = "FIELD_EMAIL"
const val FIELD_USER_NAME = "FIELD_USER_NAME"
const val FIELD_PASSWORD = "FIELD_PASSWORD"
const val FIELD_CONFIRM_PASSWORD = "FIELD_CONFIRM_PASSWORD"
}
}
MainActivity
MainActivity.kt parte funcional de entrada de datos
class MainActivity : AppCompatActivity() {
private val myValidator = ValidationMainFields(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnAutoUsername.setOnClickListener {
val s = "bob" + (0..100).random()
edtUserName.setText(s)
}
myValidator.setOnValidateListener(validatorListener())
edtEmail.onChangeDebounce {
myValidator.checkFieldEmail(it)
myValidator.getAssertion(ValidationMainFields.FIELD_EMAIL)?.let {
til1.error = it.error
}
}
edtUserName.onChangeDebounce {
myValidator.checkFieldUserName(it)
myValidator.getAssertion(ValidationMainFields.FIELD_USER_NAME)?.let {
til2.error = it.error
}
}
edtPassword.onChangeDebounce {
myValidator.checkFieldPassword(it)
myValidator.getAssertion(ValidationMainFields.FIELD_PASSWORD)?.let {
til3.error = it.error
}
}
edtCofirmPassword.onChangeDebounce {
myValidator.checkFieldConfirmPassword(edtPassword.text.toString(), it)
myValidator.getAssertion(ValidationMainFields.FIELD_CONFIRM_PASSWORD)?.let {
til4.error = it.error
}
}
btnValidate.setOnClickListener {
myValidator.checkFieldEmail(edtEmail.text.toString())
myValidator.checkFieldUserName(edtUserName.text.toString())
myValidator.checkFieldPassword(edtPassword.text.toString())
myValidator.checkFieldConfirmPassword(edtPassword.text.toString(), edtCofirmPassword.text.toString())
myValidator.validate()
}
edtCofirmPassword.onDone {
btnValidate.performClick()
}
}
private fun validatorListener(): ValidatorFieldHelper.ValidatorFieldsListener {
return object : ValidatorFieldHelper.ValidatorFieldsListener {
override fun onSuccessfulValidator() {
toast("onSuccessfulValidator")
til1.error = null
til2.error = null
til3.error = null
til4.error = null
}
override fun onErrorValidator(assertionList: HashMap<String, ValidatorFieldHelper.AssertionItem>) {
toast("Validator error")
Log.w(TAG, "onErrorValidator() called with: assertionList = [$assertionList]")
assertionList[ValidationMainFields.FIELD_EMAIL]?.let {
til1.error = it.error
}
assertionList[ValidationMainFields.FIELD_USER_NAME]?.let {
til2.error = it.error
}
assertionList[ValidationMainFields.FIELD_PASSWORD]?.let {
til3.error = it.error
}
assertionList[ValidationMainFields.FIELD_CONFIRM_PASSWORD]?.let {
til4.error = it.error
}
}
}
}
//Kotlin extensions
fun Context?.toast(text: CharSequence, duration: Int = Toast.LENGTH_SHORT) =
this?.let { Toast.makeText(it, text, duration).show() }
fun Context?.toast(@StringRes textId: Int, duration: Int = Toast.LENGTH_SHORT) =
this?.let { Toast.makeText(it, textId, duration).show() }
fun TextInputEditText.onChange(cb: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
cb(s.toString())
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}
fun TextInputEditText.onChangeDebounce(duration: Long = 350L, cb: (String) -> Unit) {
var lastStr = ""
this.addTextChangedListener(object : 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) {
val newStr = s.toString()
if (newStr == lastStr)
return
lastStr = newStr
GlobalScope.launch(Dispatchers.Main) {
delay(duration)
if (newStr != lastStr)
return@launch
if (isAttachedToWindow) cb(s.toString())
}
}
})
}
fun TextInputEditText.onDone(callback: () -> Unit) {
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
callback.invoke()
}
false
}
}
companion object {
private val TAG = MainActivity::class.java.simpleName
}
}
Muestra de la plantilla
