CategoriesArchitecture

Android MVVM architecture with clean error handling – Part 2

I wrote an article a few months back explaining my approach to error handling in MVVM architecture. since that, I had a chance to apply this approach to a few projects I was working on. While my approach still holds strong I have noticed a few places where I can improve to make this approach even better. These changes I’m about to make still follow the same flow as I had in my first article.

More control in Handling processing status

In the previous article, I had a boolean mutable live data variable in BaseViewModel which is used by the UI layer to show the loading animation on the screen to notify the user that the application is doing some processing. The problem with having a boolean value is that on some screens you will need to do different things based on the status of “completed”, “processing” and “error”. Obviously, this boolean value can not store 3 values for UI to refer to and do the necessary changes base on status. To handle this I have to introduce the below enum class.

enum class ProcessingStatus {
	PROCESSING,COMPLETED,ERROR
}

According to this enum introduction, I changed the mutable live data variable type we used in BaseViewModel. After changing isProcessing variable value in BaseViewModel to ProcessingStatus type we can set up that error status anywhere on inherited ViewModel classes. UI layer can refer to it and show the appropriate animation to inform the user what’s going on in the app. With the above variable type change, we need to change the BaseViewModel constructor function like below. 

init {

    operationErrorLiveData.addSource(repository.operationErrorLiveDate){
			isProcessing.value= ProcessingStatus.ERROR
			operationErrorLiveData.value=it
    }

		operationErrorLiveData.addSource(_operationErrorLiveData){
			isProcessing.value= ProcessingStatus.ERROR
			operationErrorLiveData.value=it
    }
}

After this, I added changes to the BaseFragment class since it is responsible for observing the isProcessing live data value from the ViewModel layer. The first change is adding an additional parameter for the initialization function so that the fragment that inherits the  BaseFragment can provide a function that contains the operations that need to happen in case of error. Secondly, I changed the observeProcessingStatus function to catch the ERROR enum type and invokes that provided function into initialization which contains the instructions to execute on a processing error.

protected open fun initialization(onDataProcessing: (() ->Unit)?,
                                  onDataProcessingComplete: (() ->Unit)?,
                                  onDataProcessingError: (() ->Unit)?) {
    observeOperationError()
    observeProcessingStatus(onDataProcessing,onDataProcessingComplete,onDataProcessingError)
}

private fun observeProcessingStatus(
    onDataProcessing: (() -> Unit)?,
    onDataProcessingComplete: (() -> Unit)?,
    onDataProcessingError: (() ->Unit)?
) {
    viewModel.isProcessing.observe(viewLifecycleOwner,{

when (it) {
            ProcessingStatus.PROCESSING -> {
                onDataProcessing?.invoke()
            }
            ProcessingStatus.COMPLETED -> {
                onDataProcessingComplete?.invoke()
            }
            ProcessingStatus.ERROR -> {
                onDataProcessingError?.invoke()
            }
            else ->{
                Throwable("null processing status")
            }
        }
})
}

In this project example, there is no need to handle the Error state. So I renamed the function to onDataProcessingComplete to onDataProcessingCompleteOrError and pass that value like below in the initialization().

private fun initialization() {

    fragmentSubComponent = injector.fragmentComponent().create(requireView())
    fragmentSubComponent.inject(this)

    super.initialization({onDataProcessing()},
												 {onDataProcessingCompleteOrError()},
												 {onDataProcessingCompleteOrError()})
}

Real-time error handling.

Something I didn’t think about when I initially constructed this error handling flow was how to handle the real-time validation. At that time it didn’t occur to me that it would require many changes to the existing classes. I was wrong about the assumption. let’s take a look at how to handle real-time validation.

In the current code, I have all the validation logic inside a single function in the RegisterViewModel . To achieve real-time validation we need to separate these validation logics and trigger relevant validation based on the edit text field text change. In this method, I still use fieldErrors to store the validation errors that occur based on the user input. the fieldErrors field can be moved into the BaseViewModel. In the previous example fieldErrors was defined as a type of mutableMapOf<String, Any> () and maintained the errors by adding and removing the values from it. This time I made a change to that value by making it mutableMapOf<String, Any?> () type and filed is identified as a valid edit text field if its value is null. With these changes, OperationError class needs to change like below. 

class OperationError private constructor(
        val errorType: OperationErrorType,
        val errorId: Int? = 1,
        val messageTitle: String? = null,
        val message: String? = null,
        val fieldErrors: Map<String, Any?>? = null) {

    data class Builder(
            var errorType: OperationErrorType,
            var errorId: Int? = 1,
            var messageTitle: String? = null,
            var message: String? = null,
            var fieldErrors: Map<String, Any?>? = null) {

        fun errorId(errorId: Int) = apply{this.errorId = errorId}
fun messageTitle(messageTitle: String) = apply{this.messageTitle = messageTitle}
fun message(message: String) = apply{this.message = message}
fun fieldError(fieldErrors: Map<String, Any?>) = apply{this.fieldErrors = fieldErrors}

fun build() = OperationError(
                errorType,
                errorId,
                messageTitle = messageTitle,
                message = message,
                fieldErrors = fieldErrors
        )
    }
}

As I mentioned earlier we need to separate the validation logic into separate functions on the ViewModel layer. There are two instances in which we need to trigger the validation function. Whenever any of the edit text in the form is changed the relevant I validation method for that function should be triggered. The other instance is when the user clicks the “Register” button. When the user clicks on the “Register” button all field validation should be triggered. This may seem unnecessary since the user will go through the form items and it gets validated when the application supports real-time validation. But all validations are triggered to show the user all the errors in case the user clicks the “Register” button without checking the form. Like in the previous example I used validationError(fieldErrors) function to push error data to the UI layer. The changed ViewModel class will look like below.

fun registerUser(requestBody: RegisterRequest){

        viewModelScope.launch {
            isProcessing.value = ProcessingStatus.PROCESSING
            repository.registerUser(requestBody)
        }

    }

fun usernameValidation(userName: String){

        if(userName.isBlank()){
            fieldErrors[usernameErrorKey] = R.string.empty_user_name_error
        }else if(userName.matches(".*\\s.*".toRegex())){
            fieldErrors[usernameErrorKey] = R.string.user_name_with_space_error
        }else{
            fieldErrors[usernameErrorKey] = null
        }

        validationError(fieldErrors)
    }

    fun emailValidation(email: String){

        if(email.isBlank()){
            fieldErrors[emailErrorKey] = R.string.empty_email_error_message
        }else if(email.matches(".*\\s.*".toRegex())){
            fieldErrors[emailErrorKey] = R.string.email_with_space_error
        }else if(!android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()){
            fieldErrors[emailErrorKey] = R.string.invalid_email_error
        }else{
            fieldErrors[emailErrorKey] = null
        }

        validationError(fieldErrors)
    }

    fun passwordValidation(password: String){

        if(password.isBlank()){
            fieldErrors[passwordErrorKey] = R.string.empty_password_error
        }else if(password.matches(".*\\s.*".toRegex())){
            fieldErrors[passwordErrorKey] = R.string.password_with_space_error
        }else if(password.length <= 8){
            fieldErrors[passwordErrorKey] = R.string.password_length_error
        }else if(!password.matches(".*\\d.*".toRegex())){
            fieldErrors[passwordErrorKey] = R.string.missing_number_in_password_error
        }else if(password == password.toLowerCase(Locale.ROOT)){
            fieldErrors[passwordErrorKey] = R.string.missing_upper_case_letter_in_password_error
        }else{
            fieldErrors[passwordErrorKey] = null
        }

        validationError(fieldErrors)
    }

    fun confirmPasswordValidation(password: String ,confirmPassword: String){

        if(confirmPassword.isBlank()){
            fieldErrors[confirmPasswordKey] = R.string.empty_confirm_password_error
        }else if(confirmPassword.matches(".*\\s.*".toRegex())){
            fieldErrors[confirmPasswordKey] = R.string.confirm_password_with_space_error
        }else if(confirmPassword != password){
            fieldErrors[confirmPasswordKey] = R.string.password_mismatch_error
        }else{
            fieldErrors[confirmPasswordKey] = null
        }

        validationError(fieldErrors)

    }

    fun validateRegisterForm(requestBody: RegisterRequest) {

        usernameValidation(requestBody.userName)
        emailValidation(requestBody.email)
        passwordValidation(requestBody.password)
        confirmPasswordValidation(requestBody.password, requestBody.confirmPassword)

    }

Let’s take a look at the changes I added to the UI layer. First, I have added a function for each input field to trigger the relevant validation function on the ViewModle on filed text change. I have used doAfterTextChanged the extension function on EditTextView for this. Then I have changed the showFormErrors function which is responsible for showing the error message under the relevant input field. This time key values are not removed from fieldErrors . In order to identify whether there is an error to show check the fieldErrors with the relevant key and check if its value is null. If the value is null that means there is no error to show.

private fun usernameChangeListener(){

    viewBinding.userNameEditText.doAfterTextChanged{
viewModel.usernameValidation(it.toString())
}

}

private fun emailChangeListener(){

    viewBinding.emailEditText.doAfterTextChanged{
viewModel.emailValidation(it.toString())
}
}

private fun passwordChangeListener(){

    viewBinding.passwordEditText.doAfterTextChanged{
viewModel.passwordValidation(it.toString())
}

}

private fun passwordConfirmChangeListener(){

    viewBinding.passwordConfirmEditText.doAfterTextChanged{
viewModel.confirmPasswordValidation(
            it.toString(),
            viewBinding.passwordEditText.text.toString()
        )
}

}

private fun showFormErrors(fieldErrors: Map<String, Any?>) {

        fieldErrors.forEach {

            when (it.key) {
                RegisterViewModel.usernameErrorKey -> {
                    viewBinding.userNameInputLayout.isErrorEnabled = it.value != null
                    if(it.value != null) {
                        viewBinding.userNameInputLayout.error = resolveErrorResource(it.value!!)
                    }
                }
                RegisterViewModel.emailErrorKey -> {
                    viewBinding.emailInputLayout.isErrorEnabled = it.value != null
                    if(it.value != null) {
                        viewBinding.emailInputLayout.error = resolveErrorResource(it.value!!)
                    }
                }
                RegisterViewModel.passwordErrorKey -> {
                    viewBinding.passwordInputLayout.isErrorEnabled = it.value != null
                    if(it.value != null) {
                        viewBinding.passwordInputLayout.error = resolveErrorResource(it.value!!)
                    }
                }
                RegisterViewModel.confirmPasswordKey -> {
                    viewBinding.passwordConfirmInputLayout.isErrorEnabled = it.value != null
                    if(it.value != null){
                        viewBinding.passwordConfirmInputLayout.error = resolveErrorResource(it.value!!)
                    }
                }
            }
        }

    }

Finally, onClickRegisterButton() function needs to be changed which is responsible for triggering registerUser(requestBody: RegisterRequest) function. In our previous example, all filed validations are checked before performing a network call on the ViewModel layer. I have moved that part into the UI layer. As I mentioned before there is a chance the user clicks on the “Register” button without going through the input fields. In that case, I have triggered the validateRegisterForm(requestBody: RegisterRequest)  function so that it will add proper validations errors to each field. By using a simple filter we determine whether there are any errors. Changed onClickRegisterButton will look like this.

private fun onClickRegisterButton() {

    viewBinding.registerButton.setOnClickListener{

val username = viewBinding.userNameEditText.text.toString()
        val email = viewBinding.emailEditText.text.toString()
        val password = viewBinding.passwordEditText.text.toString()
        val passwordConfirm = viewBinding.passwordConfirmEditText.text.toString()

        viewBinding.userNameInputLayout.error = null
        viewBinding.userNameInputLayout.isErrorEnabled = false
        viewBinding.emailInputLayout.error = null
        viewBinding.emailInputLayout.isErrorEnabled = false
        viewBinding.passwordInputLayout.error = null
        viewBinding.passwordInputLayout.isErrorEnabled = false
        viewBinding.passwordConfirmInputLayout.error = null
        viewBinding.passwordConfirmInputLayout.isErrorEnabled = false

        val registerRequest = RegisterRequest(username, email, password, passwordConfirm)
        viewModel.validateRegisterForm(registerRequest)

        if (viewModel.fieldErrors.filter{it.value != null}.isEmpty()) {
            viewModel.registerUser(registerRequest)
        }

}

}

Handle Internet connection issue

In our previous example, I handled all the network issues that occur when the application hits the API endpoints. But I have missed the time-out and no connection scenarios. In this design, all the operation-related issues are handled in the repository layer. These changes are made to the code while maintaining that. Handling the time-out exception is an easy task all we have to do is add that exception to the try-catch block. But to detect whether the mobile device has an internet connection requires a bit more work.

To keep all the exceptions in the same place, the device’s internet connection status check logic should be placed in the repository layer. But doing so will require access to the context. But we do not have access to the context object in the repository layer. So we need to look into an alternative solution. The solution I came up with was to add Interceptor to OkHttp and check the network status in that. This class will trigger every time the application tries to do a network call and throw a custom exception if no network is available to perform API calls. That exception can be caught in the repository layer and handled that from there like other network-related issues. 

class ConnectivityInterceptor(val context: Context): Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        val request: Request = chain.request()

        if(!isDeviceOnline(context)){
            throw NoConnectivityException()
        }

        return chain.proceed(request)
    }

    private fun isDeviceOnline(context: Context) : Boolean{

        val connectivityManager =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        val networkCapabilities = connectivityManager.activeNetwork?: return false

        val actNw =
            connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false

        return when {
            actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
            actNw.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) -> true
            else -> false
        }
    }

    inner class NoConnectivityException: IOException()
}

Create the ConnectivityInterceptor class like above. I have created a custom exception called NoConnectivityException this is the exception that I need to catch on try-catch block. Next, add the Interceptor like below to OkHttp . NetworkService class is changed as below to add ConnectivityInterceptor. In order for this to work, ACCESS_NETWORK_STATE permission should be given.

class NetworkService {

    private var baseURL: String = ""
    private lateinit var connectivityInterceptor: ConnectivityInterceptor

    companion object {
        fun getInstance(context: Context): NetworkService {
            return NetworkService(context)
        }

        fun getTestInstance(context: Context,testUrl: HttpUrl): NetworkService {
            return NetworkService(context, testUrl)
        }
    }

    constructor(context: Context){

        connectivityInterceptor = ConnectivityInterceptor(context)
        baseURL = when(ThisApplication.buildType){
            BuildType.RELEASE -> Config.LIVE_BASE_URL
            BuildType.DEVELOPMENT -> Config.DEV_BASE_URL
            BuildType.TESTING -> ""
        }

    }

    constructor(context: Context, testUrl: HttpUrl) : this(context) {

        connectivityInterceptor = ConnectivityInterceptor(context)
        baseURL = testUrl.toString()

    }

    fun <S> getService(serviceClass: Class<S>): S {

        val httpClient = OkHttpClient.Builder()
                .addNetworkInterceptor(StethoInterceptor())
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .addInterceptor(connectivityInterceptor)
                .addInterceptor(MockInterceptor())

        val builder = Retrofit.Builder()
                .addConverterFactory(MoshiConverterFactory.create())
                .baseUrl(baseURL)
                .client(httpClient.build())

        val retrofit = builder.build()

        return retrofit.create(serviceClass)
    }
}

Now newly added exceptions can be catch on any function that perform a network calls on repository class. 

} catch (exception: Exception) {

    when (exception) {
        is HttpException -> connectionError(exception.message())
        is ConnectivityInterceptor.NoConnectivityException -> noConnectivityError()
        is SocketTimeoutException -> timeoutConnectionError()
        is IOException -> processingError()
        else -> processingError()
    }

}

I have added timeoutConnectionError() and noConnectivityError() functions to the BaseViewModel class like below.

fun timeoutConnectionError(errorId: Int = 1) {

    val operationError = OperationError
        .Builder(OperationErrorType.CONNECTION_ERROR)
        .errorId(errorId)
        .messageTitle("Connection Error")
        .message("Failed to connect to the server please try again later.")
        .build()

    operationErrorLiveDate.value = SingleLiveEvent(operationError)
}

fun noConnectivityError(errorId: Int = 1) {

    val operationError = OperationError
        .Builder(OperationErrorType.CONNECTION_ERROR)
        .errorId(errorId)
        .messageTitle("Connection Error")
        .message("Device is not connected to the internet. Please check your mobile internet connection.")
        .build()

    operationErrorLiveDate.value = SingleLiveEvent(operationError)
}

Now, whenever a time out occurs or if the device is not connected to the internet dialog box will pop up with a relevant error message. This concludes all the changes I made to this project to improve its function. I have made these changes based on the challenge I have experienced when I apply this solution to real-world applications. I believe this holds very well for any application as a scalable and flexible solution for error handling. Please find the full code for the project here. Please refer the “error_handling_part_2” branch.

Published by Chathuranga Shan

I am a mobile application developer who loves to get into new technologies in the Industry. Specialized in Android mobile application development with 5+ years’ experience working for different types of products across many business domains.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.