How ViewModel could catch exception not thrown by the above layer?

Viewed 1190

Currently developing an Android application in Kotlin I decided to learn the MVVM pattern.

I'm in a situation where I have :

Fragment --> ViewModel --> Controller --> Repository

My controller only serves as a cache for the current session.

Currently able to contact my API, I wanted to handle potential exceptions that might occur such as "UnknownHostException", "SocketTimeoutException" and display a different message for each Exception.

NB : I used retrofit 2.6.0 with coroutines to do the job

The steps to request contact to the API are as follows:

  • The fragment subscribes to the viewModel variables

  • The viewModel launches its "fetchCommandList" method where it launches a coroutine with a try/catch and asks the controller to return the list of commands.

  • The controller calls the repository if necessary.

  • The repository tries to retrieve the data in a try/catch block

And that's where my question comes in. If you've read correctly, my controller doesn't have a try/catch, however, if in the repository an exception is thrown, the viewModel catches it well and changes its LiveData which displays the specific message of the exception in the fragment.

I don't understand how the exception is able to "skip" the Controller layer and still arrive in the viewModel.

Below is the code for each layer, from the view to the model:

Fragment part about error LiveData

mviewModel.mErrorStatus.observe(viewLifecycleOwner, Observer {
        mbinding.fragmentCommandListRecyclerview.visibility = if (it != null) View.GONE else View.VISIBLE
        mbinding.fragmentCommandListErrorTextView.visibility = if (it != null) View.VISIBLE else View.GONE
        mbinding.fragmentCommandListErrorTextView.text = it
    })

ViewModel call to the controller

fun fetchCommandList(){
    //TODO : contact the controller method tha will firstly check is in list & if not result ask the repository to retrieve it
    viewModelScope.launch {
        mProgressBar.postValue(true)
        try {
            mCommandList.postValue(CommandControllerMock.getCommandsList())
            mCommandActive.postValue(CommandControllerMock.getCurrentCommand())
        }
        catch (e: UnknownHostException) {
            //this exception occurs when there is no internet connection or host is not available
            //so inform user that something went wrong
            mErrorStatus.postValue(e.message)
        } catch (e: SocketTimeoutException) {
            //this exception occurs when time out will happen
            //so inform user that something went wrong
            mErrorStatus.postValue(e.message)
        } catch (e: Exception) {
            //this is generic exception handling
            //so inform user that something went wrong
            mErrorStatus.postValue(e.message)
        }
        finally {
            mProgressBar.postValue(false)
        }

    }

}

Controller getCommandsList called by the viewModel (NO TRY/CATCH)

override suspend fun getCommandsList(): List<Command> {
        if(m_commandList.size <= 0){
            //Todo: replace by auth.userID
            //Todo : before return the result, check if a command is already active to set currentCommand
            val returnedList = m_commandRepository.getAllCommandByUser(USER_ID)
            returnedList?.let{
                m_commandList = it.toMutableList()
            }
            checkForActiveCommand()
        }
        return m_commandList
}

Repository method calling the API endpoint

override suspend fun getAllCommandByUser(idUser: String): List<Command>? {
    try {

        val response = apiService.getAllCommandByUser(idUser)

        if (response.isSuccessful) {
            return response.body()
        } else {
            throw Exception("not 200 code")
        }
    } catch (e: Exception) {
        //this is generic exception handling
        //so inform user that something went wrong
        throw e
    }
}

So if for example, I disabled Internet on my phone I received the following message due to UnknownHostException : "Unable to resolve host "MY_API_ADRESS" : No adress associated with hostname"

If I put a readTimeout of 1sec in OkHTTP I received the following message due to SocketTimeoutException : "timeout"

I just don't understand the part where I forget to throw the exception in controller but it still catch by the ViewModel .

1 Answers

It is natural for suspended functions to propagates runtime exceptions to top caller, until it either hits a try and catch block or coroutine scope, which is then either handled by exceptionHandler or cause the app to crash.

So in your case, nothing is suspicious, and the try&catch with exception rethrowing in repository layer is unnecessary.

See Coroutines exception handling doc

Related