Easy Validations In Android’s Modular App Architectures

Mohamed Hamdan
Winfooz’s Engineering Blog
5 min readOct 16, 2019
When you are building an Android application with lots of forms of data, the first thing you should think about is user input validations for better user experience.

Adding validations to different types of views requires a developer to repeating himself because he has to create all views one more time to handle validation on all views or write lots of code when in every ViewModel.

What if there is another way to handle user input validations just by writing custom annotations, for example, @Email, @Password, @CheckedIt will be more effective, isn’t it?.

In our company, we were using android-saripaar for user input validations and it was working fine until we decided to move to modular android app architecture.

Also, we’re using localized strings for validation messages in our modular android projects, so in that case we can’t useR.java with java annotations because in the library projects because all fields inside R.java is not final so we have created our validation library to support modular projects, writing less code with the usage of JCTree with R2.java.

Let’s start adding dependencies, You can check the latest version here.

allprojects {repositories {
...
maven {
url "https://dl.bintray.com/mnayef95/WinValidation"
}
}
}
dependencies {
...
implementation 'com.winfooz:validation:1.0.0-beta1'
kapt 'com.winfooz:validation-compiler:1.0.0-beta1'
}

Now you are ready to use WinValidation.

let’s start with small form

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Design.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="26dp"
android:layout_marginEnd="20dp"
android:hint="Email">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_text_fragment_login_email"
android:layout_width="match_parent"
android:layout_height="58dp"
android:imeOptions="actionNext"
android:inputType="textEmailAddress"
android:nextFocusDown="@+id/edit_text_fragment_login_password">
<requestFocus />
</com.google.android.material.textfield.TextInputEditText>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Design.TextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="20dp"
android:hint="Email"
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_text_fragment_login_password"
android:layout_width="match_parent"
android:layout_height="58dp"
android:imeOptions="actionDone"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_fragment_login_submit"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="28dp"
android:padding="0dp"
android:text="Submit"
android:textAllCaps="false"
android:textColor="?colorAccent"
android:textSize="14sp" />
</RelativeLayout>

Now you are ready to start validation, going to Java/Kotlin code and you can use annotations working with annotation processing instead of reflection to achieve what you want.

Lets writing some code

@Email(R.id.edit_text_fragment_login_email, messageResId = R.string.err_email_validation)
var editTextEmail: EditText? = null

As you notice the edit text annotated with @Email, any validation annotation requires two parameters the first one is the view id and the second one is message resource id which will appear if the user input is wrong, if you are wondering why there is a view id with an annotation that because of the WinValidation will do findViewById for you.

Now you have to add @Validations annotation at top of your class name

@Validations
class LoginActivity : AppCompatActivity() {
...
}

Now all you need to do is adding action to your button for validation callback

@ValidateOnClick(R.id.button_fragment_login_submit)
fun onValidationSucceeded() {
...
}

Now onValidationSucceeded() method will be invoked only if the validation succeeded if you want a callback for the failed scenario you can add a boolean parameter for the method and WinValidaition will invoke the method and pass true/false based on the validation result.

@ValidateOnClick(R.id.button_fragment_login_submit)
fun onValidationSucceeded(success: Boolean) {
...
}

Auto validation

WinValidation supports for auto validation while user typing for example if your edit text annotated with @Email and user typing wrong email syntax once the user focus up the validation message will appear to indicate that the email was entered is wrong and once the user starts editing the email the auto validation will remove the error message once the email form is correct

In WinValidation auto validation is disabled by default to enable it you need to add @AutoValidation annotation to the edit text

@AutoValidate
@Email(R.id.edit_text_fragment_login_email, messageResId = R.string.err_email_validation)
var editTextEmail: EditText? = null

Once you add this annotation the auto validation will be enabled.

Write your custom annotation

The WinValidation library support for adding custom annotations, let’s say you have a Phone edit text working only of user-entered Canadian number

Let’s create our custom annotation class

@Adapter(EditTextAdapter::class)
@AutoValidateWith(EditTextAutoValidate::class)
@Rule(PhoneValidationRule::class)
@ErrorHandling(EditTextErrorHandling::class)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class Phone(@IdRes val value: Int, @StringRes val messageResId: Int)

Now we have created the phone annotation class

Adapter: The adapter is the class responsible for collecting the data from the view, which is the EditText in this example

AutoValidateWith: The auto validate with is the class who is responsible for adding a value change listener, in this scenario will be TextWatcher

Rule: The rule is the class who is responsible for validating if the data is correct or not, in this scenario will be responsible for checking if the phone is a valid Canadian phone number or not.

ErrorHandling: The error handling is the class who is responsible for showing the error to the user

WinValidaiton contains predefined adapters, error handling, and auto validation classes you can use them inside your project.

Now you need to create the PhoneValidationRule class for check if the phone is a Canadian phone or not by using google phone validation library

class PhoneValidationRule : ValidationRule<CharSequence, Phone> {

private val countries = mutableListOf("CA")

override fun isValid(data: CharSequence?, annotation: Phone?): Boolean {
val utils = PhoneNumberUtil.getInstance()
return try {
countries.forEach {
if (utils.isValidNumberForRegion(utils.parse(data.toString(), it), it)) {
return true
}
}
false
} catch (ignored: Exception) {
false
}
}
}

As you see the isValid method contains two arguments the first one is the data collected by the adapter and the second is the annotation itself so if you have any additional information you could add it to the annotation and read it from the rule class same as @Password annotation.

@Adapter(EditTextAdapter::class)
@AutoValidateWith(EditTextAutoValidate::class)
@Rule(PasswordValidationRule::class)
@ErrorHandling(EditTextErrorHandling::class)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class Password(
@IdRes val value: Int,
@StringRes val messageResId: Int,
val scheme: Scheme = Scheme.ANY,
val min: Int = 6
) {

enum class Scheme {

ANY,
ALPHA,
ALPHA_MIXED_CASE,
NUMERIC,
ALPHA_NUMERIC,
ALPHA_NUMERIC_MIXED_CASE,
ALPHA_NUMERIC_SYMBOLS,
ALPHA_NUMERIC_MIXED_CASE_SYMBOLS,
ALPHA_NUMERIC_ALLOW_SYMBOLS_WITHOUT_SPACE
}
}

As you see the password annotation contains another two arguments scheme and min which will be read from the rule class as this

override fun isValid(data: CharSequence?, annotation: Password?): Boolean {
if (data == null || annotation == null) {
return false
}
val hasMinChars = data.length >= annotation.min
val matchesScheme = SCHEME_PATTERNS[annotation.scheme]?.toRegex()?.matches(data) == true
return hasMinChars && matchesScheme
}
private companion object {

private val SCHEME_PATTERNS = mapOf(
Scheme.ANY to ".+",
Scheme.ALPHA to "\\w+",
Scheme.ALPHA_MIXED_CASE to "(?=.*[a-z])(?=.*[A-Z]).+",
Scheme.NUMERIC to "\\d+",
Scheme.ALPHA_NUMERIC to "(?=.*[a-zA-Z])(?=.*[\\d]).+",
Scheme.ALPHA_NUMERIC_MIXED_CASE to "(?=.*[a-z])(?=.*[A-Z])(?=.*[\\d]).+",
Scheme.ALPHA_NUMERIC_SYMBOLS to "(?=.*[a-zA-Z])(?=.*[\\d])(?=.*([^\\w]|_)).+",
Scheme.ALPHA_NUMERIC_MIXED_CASE_SYMBOLS to "(?=.*[a-z])(?=.*[A-Z])(?=.*[\\d])(?=.*([^\\w]|_)).+",
Scheme.ALPHA_NUMERIC_ALLOW_SYMBOLS_WITHOUT_SPACE to "^(?=.*[0-9])(?=.*[A-Za-z!@#$%^&*()_+])(?=\\S+$).+$"
)
}

Library project

In library projects, we can’t use R.java with annotations because its fields are not final and Java annotations require all values to be final, so we need another way to handle this case, Butterknife has its solution for this problem and we will use it.

For library projects add Butterknife’s gradle plugin to your buildscript.

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:10.1.0'
}
}

and then apply it in your module:

...
apply plugin: 'com.jakewharton.butterknife'

Now make sure you use R2 instead of R inside all WinValidation annotations.

@Validations
class TestActivity : Activity() {

@AutoValidate
@NotEmpty(R2.id.username, messageResId = R2.string.err_username_validation)
var editTextUsername: TextInputEditText? = null

....
}

Conclusion

Validations are now easily added to different views with the ability to auto validate. And all of that was achieved using annotations allowing us to write reusable, less code.

References

https://jakewharton.github.io/butterknife/

--

--