Validación: Un caso práctico

Gerardo Prado
Sep 3, 2018 · 4 min read

Los invito a leer la introducción en https://medium.com/kotlin-sin-java/el-problema-kotlin-javification-fde52ec6687d

El siguiente código es un ejemplo de validación de un conjunto de campos TextInputLayout escrito en Java

public class ExampleActivity extends AppCompatActivity {    private Button buttonContinue;
private TextInputLayout tInputName, tInputEmail, tInputPhone;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/* findViewById(...) */ buttonContinue.setOnClickListener(view -> {
EditText eTextName = tInputName.getEditText();
EditText eTextEmail = tInputEmail.getEditText();
EditText eTextPhone = tInputPhone.getEditText();
assert eTextName != null;
assert eTextEmail != null;
assert eTextPhone != null;
String name = eTextName.getText().toString().trim();
String email = eTextEmail.getText().toString().trim();
String phone = eTextPhone.getText().toString().trim();

if (areFieldsValid(name, email, phone)) {
saveData(name, email, phone);
}
});
}
private boolean areFieldsValid(String name, String email, String phone) {
if (name.isEmpty()) {
tInputName.setError("Campo obligatorio");
tInputName.requestFocus();
return false;
}
if (email.isEmpty()) {
tInputEmail.setError("Campo obligatorio");
tInputEmail.requestFocus();
return false;
}
if (!isValidEmail(email)) {
tInputEmail.setError("Email inválido"));
tInputEmail.requestFocus();
return false;
}
if (phone.isEmpty()) {
tInputPhone.setError("Campo obligatorio");
tInputPhone.requestFocus();
return false;
}
if (!isValidPhone(phone)) {
tInputPhone.setError("Teléfono inválido");
tInputPhone.requestFocus();
return false;
}
return true;
}
private boolean isValidPhone(String value) {
/* Lógica de validación */
}
private boolean isValidEmail(String value) {
/* Lógica de validación */
}
private void saveData(String name, String email, String phone) {
/* Lógica de guardado */
}
}

Hacer una traducción literal de Java a Kotlin nos haría perder los beneficios funcionales que Kotlin nos ofrece, lo bueno es que estamos acá para salvar al mundo de este sufrimiento, y ponernos manos a la obra en una traducción que aproveche en mayor medida las herramientas que Kotlin nos brinda, para este caso usaremos Extension Functions y Custom Infix Operators

Lo primero es lo primero, para escribir extension functions que sean re-utilizables e independientes del contexto, debemos identificar ¿qué hace?, ¿de qué depende? y además pensar de forma funcional, en nuestro caso tenemos siempre 3 elementos presentes, un campo que se somete a prueba (TextInputLayout), la prueba a la que será sometido (TextInputLayout -> Boolean) y el texto de error que corresponde a esta prueba (StringRes), creemos la clase:

class Validation(val view: TextInputLayout,
@StringRes val resource: Int,
val validator: (TextInputLayout.() -> Boolean))

Crear una instancia de esta clase usando su constructor directamente podría ser un desperdicio de expresividad del lenguaje, seamos un poco más creativos, el código final nos lo agradecerá, se me ocurre algo como:

“Para el campo X asigna el error Y cuando se cumpla la condición Z

Podemos notar que en esta oración se encuentran nuestros 3 elementos: campo, error y condición; unidos por lo que parecen ser 2 operadores: asigna y cuando, escribamos estos operadores, comencemos con asigna:

infix fun TextInputLayout.set(@StringRes resource: Int) = this to resource

Solo hemos enmascarado el operador to como set para obtener una expresión que sea semánticamente correcta con nuestra oración anterior.

Vamos con cuando:

infix fun Pair<TextInputLayout, Int>.`when`(valid: TextInputLayout.() -> Boolean) = Validation(first, second, valid)

Un operador que dado un par <TextInputLayout, Int> y una expresión lambda en el contexto de un TextInputLayout que devuelve Boolean; este operador construye y devuelve una instancia de nuestra clase Validation; gracias a estos dos operadores, la siguiente línea tiene sentido semántico completo con respecto a nuestra oración:

tInputName set R.string.error_empty `when` { text().isEmpty() }

No sólo es una expresión válida y que literalmente describe su funcionalidad sino que además constituye una instancia de la clase Validation.

Para simplificar el acceso a la propiedad texto del TextInputLayout podemos implementar la siguiente extensión:

fun TextInputLayout.text(value: String? = null) = value?.also { editText?.setText(it) } ?: "${editText?.text}"

Ya que tenemos nuestra expresión lista, podemos construir un arreglo con nuestras validaciones:

private val validations by lazy {
arrayOf(
tInputName set R.string.error_empty `when` { text().isEmpty() },
tInputEmail set R.string.error_empty `when` { text().isEmpty() },
tInputEmail set R.string.error_email `when` { !text().isValidEmail() },
tInputPhone set R.string.error_empty `when` { text().isEmpty() },
tInputPhone set R.string.error_phone `when` { !text().isValidPhone() }
)
}

Definidas las extensiones isValidEmail e isValidPhone sobre String.

Este arreglo tan solo representa las validaciones necesarias para determinar si los campos contienen el formato deseado, pero no es en sí mismo la ejecución de la validación, vamos a realizar la implementación necesaria para ejecutar esta serie de validaciones:

fun Array<Validation>.firstInvalid(onFound: (TextInputLayout.() -> Unit)? = null) = firstOrNull { set ->
set.run {
validator
(view).also { invalid ->
if
(invalid) {
with (view) {
error = context.getString(set.resource)
onFound.notNull { it.invoke(this) }
}
}
}
}
}

Una extensión sobre un arreglo de validaciones que intenta encontrar el primer validador que devuelva verdadero, si lo encuentra ejecuta sobre ese TextInputLayout el lambda onFound y devuelve la instancia Validation que encontró el error, si no lo encuentra devuelve nulo; como suponemos que todos los casos de uso están definidos en el arreglo, si esta función devuelve nulo podemos asumir que no existe error alguno.

Estas dos extensiones nos ayudarán un poco con la expresión final:

inline fun Any?.isNull(exec: () -> Unit) = this ?: exec()inline fun <T : Any> T?.notNull(exec: (T) -> Unit) : T? = this?.apply { exec(this) }

isNull, ejecuta un lambda si el objeto es nulo.

notNull, ejecuta un lambda, delegando el objeto en it, si este no es nulo.

Finalmente tenemos nuestro resultado:

buttonContinue.onClick {
validations
.firstInvalid {
requestFocus()
}.isNull {
saveData(name = tInputName.text(),
email = tInputEmail.text(),
phone = tInputPhone.text())
}
}
}

Código altamente re-utilizable, legible, conciso y corto: es Kotlin en su máxima expresión.

Espero que les sea de utilidad este ejemplo ;)

Kotlin sin Java

Es usual que al movernos de un lenguaje de programación a otro adoptemos la forma que conocíamos de solucionar los problemas, esto se transforma en un inconveniente si ambos lenguajes no pertenecen al mismo paradigma, abordemos el caso de Java a Kotlin en Android.

Gerardo Prado

Written by

Senior Analyst at Concrete part of Accenture. Kotlin evangelist & lover of best practices

Kotlin sin Java

Es usual que al movernos de un lenguaje de programación a otro adoptemos la forma que conocíamos de solucionar los problemas, esto se transforma en un inconveniente si ambos lenguajes no pertenecen al mismo paradigma, abordemos el caso de Java a Kotlin en Android.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade