0

I want to show an error message when timeout occurs, but can't find how to do it without passing the loginProgressBar and loginTimeoutErrorTextview or other UI related variables. Basically keep the UI changes in the Fragment.

What i do in my loginFragment when pressing login

try {
    loginViewModel.login(
        binding.emailEditText.text.toString(),
        binding.passwordEditText.text.toString()
    )
} catch (e: IOException) { // THIS doesn't work
    binding.loginProgressBar.visibility = View.GONE
    binding.loginTimeoutErrorTextview.visibility = View.VISIBLE
}

From what i've found the timeout throws an IOException so everything is correct here

loginViewModel.login method:

viewModelScope.launch {
    val securedLoginRequest = encodedRequest(username, password)

    Log.i("API Login", "Sent data: $securedLoginRequest")

    try {
        Log.i("API Login", "login started")
        // _response is a LiveData<String>
        _response.value =
            ApiServiceObject.retrofitService.postLogin(securedLoginRequest)

        Log.i("API Login", "Login successful, token = ${_response.value}")
    } catch (e: IOException) {
        throw e // THIS should theoretically get catched by the block above right?
    } catch (e: Exception) {
        Log.w("API Login", e.toString())
    }
}

Problem is the thrown exception in Block 2 doesn't get catched in Block 1

I've found a workaround by simply passing down loginProgressBar and loginTimeoutErrorTextview to loginViewModel.login but is that alright? Isn't there better ways?

UPD: some clarifications

EkorrTi
  • 11
  • 3

2 Answers2

1

I've found a way, by using a StateFlow instead of LiveData in the ViewModel as follows:

private val _loginState = MutableStateFlow<LoginState>(LoginState.Empty)
val loginState = _loginState.asStateFlow()

sealed class LoginState {
    object Empty : LoginState()
    object Loading : LoginState()
    data class Success(val result: String) : LoginState()
    data class Error(val error: Throwable) : LoginState()
}

Then responding to it in the Fragment like this:

lifecycleScope.launch{
    repeatOnLifecycle(Lifecycle.State.STARTED){
        loginViewModel.loginState.collect { state ->
            binding.loginProgressBar.isGone = state !is LoginState.Loading

            when (state) {
                is LoginState.Error -> { doSomething() }
                is LoginState.Success -> { doSomething() }
                else -> Unit
            }
        }
    }
}
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
EkorrTi
  • 11
  • 3
0

A viewModelScope is a CoroutineScope. And Coroutine acts like a Thread (For more details about Coroutine you can read it here).

So you cannot have a outer try-catch like this:

try {
    viewModelScope.launch {
        ...
        throw IOException("Some IO Exception")
    }
} catch (e: IOException) {
    // It will never pass here and instead the program should crash above
    ...
}

The proper way to handle possible Exception within a Coroutine is to have the try-catch inside it:

viewModelScope.launch {
    val securedLoginRequest = encodedRequest(username, password)

    Log.i("API Login", "Sent data: $securedLoginRequest")

    try {
        Log.i("API Login", "login started")
        // _response is a LiveData<String>
        _response.value =
            ApiServiceObject.retrofitService.postLogin(securedLoginRequest)

        Log.i("API Login", "Login successful, token = ${_response.value}")
    } catch (e: IOException) {
        // You should either have a Callback to return this Exception outside this Coroutine, 
        //  or you should handle the UI update here
        // If you throw Exception here, your program should crash
    } catch (e: Exception) {
        Log.w("API Login", e.toString())
    }
}
Enowneb
  • 943
  • 1
  • 2
  • 11
  • 1
    This didn't really answer the question, as i wanted to do the UI changes in the `Fragment` to keep the UI and logic separate. But still thanks for clarification. – EkorrTi Apr 06 '23 at 08:24