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 .