So i'm trying to implement MVI pattern in android with RxJava, but i want to handle the thrown error in a state, together with success and loading, is there anyway to handle the error not from subscribe(onError = xxx)
PROCESS
sealed class AuthResult : MviResult {
sealed class LoadUserResult : AuthResult() {
object Loading : LoadUserResult()
data class Success(val user: User) : LoadUserResult()
data class Fail(val error: Throwable) : LoadUserResult()
}
}
private val loadUser =
ObservableTransformer<LoadUserAction, LoadUserResult> { actions ->
actions.flatMap {
userManager.getCurrentUser()
.map<LoadUserResult> { LoadUserResult.Success(it) }
.onErrorReturn(LoadUserResult::Fail) // HERE? // EDIT FOR THE ANSWER: REMOVE THIS
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
.startWith(LoadUserResult.Loading)
}.onErrorReturn(LoadUserResult::Fail) // ANSWER: ADD THIS TO CATCH API ERROR
}
var actionProcess =
ObservableTransformer<AuthAction, AuthResult> { actions ->
actions.publish { s->
Observable.merge(
s.ofType(LoadUserAction::class.java).compose(loadUser),
s.ofType(SignInWithGoogleAction::class.java).compose(signInWithGoogle)
)
}
}
VIEWMODEL
fun combine(): Observable<AuthViewState> {
return _intents
.map(this::actionFromIntent)
.compose(actionProcess)
.scan(AuthViewState.idle(), reducer)
.distinctUntilChanged()
.replay(1)
.autoConnect(0)
}
FRAGMENT
disposable.add(viewModel.combine().subscribe(this::response))
private fun response(state: AuthViewState) {
val user = state.user
if (user.uid.isBlank() && user.email.isBlank() && user.username.isBlank()) {
Timber.i("user: $user")
} else {
Timber.i("user: $user")
Toast.makeText(requireContext(), "Will navigate to MainActivity", Toast.LENGTH_SHORT)
.show()
}
// HANDLE THE ERROR HERE?
if (state.error != null) {
Toast.makeText(requireContext(), "Error fetching user", Toast.LENGTH_SHORT).show()
Timber.e("Error loading user ${state.error.localizedMessage}")
}
}
THE ERROR i got was
2020-06-03 22:42:15.073 25060-25060/com.xxx W/System.err: io.reactivex.exceptions.OnErrorNotImplementedException: The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | com.google.android.gms.tasks.RuntimeExecutionException: com.google.android.gms.common.api.ApiException: 10:
The error you're receiving here is due to you calling .subscribe() in your Fragment. That variant of the .subscribe() (the one that accepts only one parameter -- the onNext consumer callback) will only notify the consumer when the stream successfully emits an item (in this case, AuthViewState). However, when you observable stream encounters an error, RxJava doesn't have a good way to handle it, since an error callback was not provided in .subscribe(). Therefore, it throws the error you've encountered above.
NOTE: RxJava has many overloads of Observable.subscribe(), some of which accept a consumer callback for error handling.
However, if your goal is to have the Observable always successfully emit an AuthViewState, even if an error was encountered, you could make use of Observable.onErrorReturn() (or a similar error handling function provided by RxJava). An example usage of that would be:
sealed class ViewState {
object Loading : ViewState()
data class Success(val username: String) : ViewState()
data class Error(val error: Throwable) : ViewState()
}
class UserProfileViewModel(
private val userService: UserService
) {
fun getViewState(): Observable<ViewState> {
return Observable
.merge(
Observable.just(ViewState.Loading),
userService
.getUserFromApi()
.map { user -> ViewState.Success(user.username) }
)
.onErrorReturn { error -> ViewState.Error(error) }
}
}
Related
I have emit exception inside flow and got below exception.
IllegalStateException: Flow exception transparency is violated:
Previous 'emit' call has thrown exception java.lang.NullPointerException, but then emission attempt of value 'planetbeyond.domain.api.Resource$Error#85b4d28' has been detected.
Emissions from 'catch' blocks are prohibited in order to avoid unspecified behaviour, 'Flow.catch' operator can be used instead.
For a more detailed explanation, please refer to Flow documentation.
at kotlinx.coroutines.flow.internal.SafeCollector.exceptionTransparencyViolated(SafeCollector.kt:140)
at kotlinx.coroutines.flow.internal.SafeCollector.checkContext(SafeCollector.kt:104)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:83)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:66)
at planetbeyond.domain.use_cases.OptionSelectedCountUsecase$invoke$1.invokeSuspend(OptionSelectedCountUsecase.kt:20)
OptionSelectedCountUsecase.kt
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
try {
val data = repository.getOptionSelectedCount(questionId)
emit(Resource.Success(data))
} catch (e: Exception) {
emit(Resource.Error(e.toString()))// crashed at this line when api don't response anything or some sort of server error
}
}
}
Repository.kt
interface Repository{
suspend fun getOptionSelectedCount(questionId: Int):List<OptionSelectedCountModel>
}
RepositoryImpl.kt
class RepositoryImpl #Inject constructor(
private val apiService: ApiService
) : Repository {
override suspend fun getOptionSelectedCount(questionId: Int): List<OptionSelectedCountModel> {
return apiService.getOptionSelectedCount(questionId).data.map {
it.toModel()
}
}
}
ApiService.kt
interface ApiService {
#GET("get_option_selected_count")
suspend fun getOptionSelectedCount(
#Query("question_id") question_id: Int
): WebResponse<List<OptionSelectedCountDto>>
}
LiveShowQuestionViewModel.kt
#HiltViewModel
class LiveShowQuestionsViewModel #Inject constructor(
private val optionSelectedCountUsecase: OptionSelectedCountUsecase
) : ViewModel() {
fun getOptionSelectedCount(questionId: Int) {
optionSelectedCountUsecase(questionId).onEach {
when (it) {
is Resource.Loading -> {
_optionSelectedCountState.value = OptionSelectedCountState(isLoading = true)
}
is Resource.Error -> {
_optionSelectedCountState.value = OptionSelectedCountState(error = it.message)
}
is Resource.Success -> {
_optionSelectedCountState.value = OptionSelectedCountState(data = it.data)
}
}
}///.catch { } // Why must I have to handle it here
.launchIn(viewModelScope)
}
}
Is it neccessary to handle exception outside flow like commented above. What is the best practice.
The problem is that you wrapped an emit call in try and try to emit in the matching catch block. This means that if the emit call itself throws (which ambiguously could be caused by some downstream problem with the flow) it's being instructing to emit again. This is very ambiguous and fragile behavior.
Instead, you can move your emit call(s) outside the try/catch:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
val result = try {
val data = repository.getOptionSelectedCount(questionId)
Resource.Success(data)
} catch (e: Exception) {
Resource.Error(e.toString())
}
emit(result)
}
}
Somehow, you're causing a NullPointerException in your collector. That's a separate problem to solve.
The root problem is that your
emit(Resource.Success(data))
throws an exception. When you catch that exception you are still in the "emit" block and you are trying to
emit(Resource.Error(e.toString())
So it's like emit inside emit. So yes this is wrong.
But let's get a step backward. Why there is an exception during the first emit? It looks like this data object is not properly filled with data, probably because of the issues that you mentioned (bad response etc), after it reaches the collector there is null pointer exception.
So basic flow should be
try to make the call, and catch http/parsing exception if there is one ( emit failure)
If there was no exception, validate if the object contains proper fields. If data is inconsistent emit Error
If everything is ok emit success
for example:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
try {
val data = repository.getOptionSelectedCount(questionId)
if(validateData(data)){
emit(Resource.Success(data))
}else{
// some data integrity issues, missing fields
emit(Resource.Error("TODO error")
}
} catch (e: HttpException) {
// catch http exception or parsing exception etc
emit(Resource.Error(e.toString()))
}
}
}
This ideally should be split into, to not mess with exception catching of emit:
class OptionSelectedCountUsecase #Inject constructor(
private val repository: Repository
) {
operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
emit(Resource.Loading())
emit(getResult(questionId))
}
fun getResult(questionId: Int): Resource<List<OptionSelectedCountModel>>{
try {
val data = repository.getOptionSelectedCount(questionId)
if(validateData(data)){
return Resource.Success(data)
}else{
// some data integrity issues, missing fields
return Resource.Error("TODO error"
}
} catch (e: HttpException) {
// catch http exception or parsing exception etc
return Resource.Error(e.toString())
}
}
}
You should not emit exceptions and errors manually. Otherwise the user of the flow will not know, if exception actually happened, without checking the emitted value for being an error.
You want to provide exception transparency, therefore it is better to process them on collecting the flow.
One of the ways is to use catch operator. To simplify flow collecting we will wrap the catching behavior in a function.
fun <T> Flow<T>.handleErrors(): Flow<T> =
catch { e -> showErrorMessage(e) }
Then, while collecting the flow:
optionSelectedCountUsecase(questionId)
.onEach { ... }
.handleErrors()
.launchIn(viewModelScope)
Note, that if you want to process only the errors from invocation of the use case, you can change the order of operators. The previous order allows you to process errors from onEach block too. Example below will only process errors from use case invocation.
optionSelectedCountUsecase(questionId)
.handleErrors()
.onEach { ... }
.launchIn(viewModelScope)
Read more about exception handling in flows
I have the below code in my view model class.
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableStateFlow(0)
val marketState: LiveData<State<Market>> =
retry.flatMapLatest{repo.refreshMarket()}
.map { State.Success(it) as State<T> }
.catch { error -> emit(State.Error(error)) }
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.value++
}
}
MarketRepository.kt:
fun refreshMarket() =
flow { emit(api.getMarkets()) }
.onEach { db.upsert(it) }
.flowOn(dispatchers.IO)
It works fine until a network error occurs in the repository method refreshMarket then when I call the retry() on the view model, it doesn't trigger the flatMapLatest transformer function anymore on the retry MutableStateFlow, why?
Does the flow get complete when it calls a Catch block? how to handle such situation?
You're right, catch won't continue emitting after an exception is caught. As the documentation says, it is conceptually similar to wrapping all the code above it in try. If there is a loop in a traditional try block, it does not continue iterating once something is thrown, for example:
try {
for (i in 1..10) {
if (i == 2) throw RuntimeException()
println(i)
}
} catch (e: RuntimeException) {
println("Error!")
}
In this example, once 2 is encountered, the exception is caught, but code flow does not return to the loop in the try block. You will not see any numbers printed that come after 2.
You can use retryWhen instead of catch to be able to restart the flow. To do it on demand like you want, maybe this strategy could be used (I didn't test it):
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableSharedFlow<Unit>()
val marketState: LiveData<State<Market>> =
repo.refreshMarket()
.map { State.Success(it) as State<T> }
.retryWhen { error, _ ->
emit(State.Error(error))
retry.first() // await next value from retry flow
true
}
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.tryEmit(Unit)
}
}
My Android app crashes and I see this stack trace in Logcat. It doesn't tell me which line of code is causing the problem.
2021-05-05 09:13:33.143 1069-1069/com.mycompany.app E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.mycompany.app, PID: 1069
retrofit2.HttpException: HTTP 403
at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
Is there a way to map this back to my code, to see which call to retrofit is causing it? I have a repository with code like this:
suspend fun getSomeData(): Stuff {
return withContext(Dispatchers.IO) {
val body = myRetroApi.getStuff()
...
Do I need to wrap every withContext body to make sure no Throwables escape? I thought that if something threw an exception there, it would log an error, not crash the entire app.
Edit
I messed up when asking this question and put the emphasis on wrong things. So I'm removing the "retrofit" tag. It turns out the withContext(Dispatchers.IO) call does re-throw the Exception as expected, but when the exception gets back up to viewModelScope.launch, if that block does not catch it, the app crashes.
If the exception is not handled the app will crash of course.
You can add a try catch to avoid this:
suspend fun getSomeData() {
withContext(Dispatchers.IO) {
try{
val body = myRetroApi.getStuff()
...
} catch (e : Exception){
//your code
}
...
Retrofit is giving you a 403 Unauthorized HTTP exception. It may be that the server isn't passing any additional error message or that you need to catch HttpException and check for the message. In either case, this isn't a Retrofit issue hence it's just passing the error it's getting from the server you're calling.
It's best to create a network result wrapper and a wrapper function for API calls to handle exceptions.
You can do something like this. Keep in mind, the actual implementation is completely up to you. I would however suggest using runCatching when it comes to couroutines as it handles cancellation exceptions.
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val exception: Throwable, val message: String?) : NetworkResult<Nothing>()
}
suspend fun networkCall(): String = ""
suspend fun <T> safeApiCall(block: suspend () -> T): NetworkResult<T> {
return runCatching {
withContext(Dispatchers.IO) {
block()
}
}.fold({
NetworkResult.Success(it)
}, {
when (it) {
is HttpException -> NetworkResult.Error(it, "Network error")
else -> NetworkResult.Error(it, "Some other message...")
// else -> throw it
}
})
}
suspend fun getData() {
val result: NetworkResult<String> = safeApiCall {
networkCall()
}
when (result) {
is NetworkResult.Success -> {
//Handle success
}
is NetworkResult.Error -> { //Handle error
}
}
}
runCatching uses Kotlin's built-in Result class and there are several ways of handling the result. These are just a few.
runCatching {
//.....
}.getOrElse { throwable ->
//handle exception
}
runCatching {
//.....
}.getOrThrow()
runCatching {
}.onSuccess {
}.onFailure {
}
I have an android app that I have built up an architecture similar to the Google IO App. I use the CoroutineUseCase from that app (but wrap results in a kotlin.Result<T> instead).
The main code looks like this:
suspend operator fun invoke(parameters: P): Result<R> {
return try {
withContext(Dispatchers.Default) {
work(parameters).let {
Result.success(it)
}
}
} catch (e: Throwable) {
Timber.e(e, "CoroutineUseCase Exception on ${Thread.currentThread().name}")
Result.failure<R>(e)
}
}
#Throws(RuntimeException::class)
protected abstract suspend fun work(parameters: P): R
Then in my view model I am invoking the use case like this:
viewModelScope.launch {
try {
createAccountsUseCase(CreateAccountParams(newUser, Constants.DEFAULT_SERVICE_DIRECTORY))
.onSuccess {
// Update UI for success
}
.onFailure {
_errorMessage.value = Event(it.message ?: "Error")
}
} catch (t: Throwable) {
Timber.e("Caught exception (${t.javaClass.simpleName}) in ViewModel: ${t.message}")
}
My problem is even though the withContext call in the use case is wrapped with a try/catch and returned as a Result, the exception is still thrown (hence why I have the catch in my view model code - which i don't want). I want to propagate the error as a Result.failure.
I have done a bit of reading. And my (obviously flawed) understanding is the withContext should create a new scope so any thrown exceptions inside that scope shouldn't cancel the parent scope (read here). And the parent scope doesn't appear to be cancelled as the exception caught in my view model is the same exception type thrown in work, not a CancellationException or is something unwrapping that?. Is that a correct understanding? If it isn't what would be the correct way to wrap the call to work so I can safely catch any exceptions and return them as a Result.failure to the view model.
Update:
The implementation of the use case that is failing. In my testing it is the UserPasswordInvalidException exception that is throwing.
override suspend fun work(parameters: CreateAccountParams): Account {
val tokenClient = with(parameters.serviceDirectory) {
TokenClient(tokenAuthorityUrl, clientId, clientSecret, moshi)
}
val response = tokenClient.requestResourceOwnerPassword(
parameters.newUser.emailAddress!!,
parameters.newUser.password!!,
"some scopes offline_access"
)
if (!response.isSuccess || response.token == null) {
response.statusCode?.let {
if (it == 400) {
throw UserPasswordInvalidException("Login failed. Username/password incorrect")
}
}
response.exception?.let {
throw it
}
throw ResourceOwnerPasswordException("requestResourceOwnerPassword() failed: (${response.message} (${response.statusCode})")
}
// logic to create account
return acc
}
}
class UserPasswordInvalidException(message: String) : Throwable(message)
class ResourceOwnerPasswordException(message: String) : Throwable(message)
data class CreateAccountParams(
val newUser: User,
val serviceDirectory: ServiceDirectory
)
Update #2:
I have logging in the full version here is the relevant details:
2020-09-24 18:12:28.596 25842-25842/com.ipfx.identity E/CoroutineUseCase: CoroutineUseCase Exception on main
com.ipfx.identity.domain.accounts.UserPasswordInvalidException: Login failed. Username/password incorrect
at com.ipfx.identity.domain.accounts.CreateAccountsUseCase.work(CreateAccountsUseCase.kt:34)
at com.ipfx.identity.domain.accounts.CreateAccountsUseCase.work(CreateAccountsUseCase.kt:14)
at com.ipfx.identity.domain.CoroutineUseCase$invoke$2.invokeSuspend(CoroutineUseCase.kt:21)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2020-09-24 18:12:28.598 25842-25842/com.ipfx.identity E/LoginViewModel$createAccount: Caught exception (UserPasswordInvalidException) in ViewModel: Login failed. Username/password incorrect
The full exception is logged inside the catching in CoroutineUseCase.invoke. And then again the details logged inside the catch in the view model.
Update #3
#RKS was correct. His comment caused me to look deeper. My understanding was correct on the exception handling. The problem was in using the kotlin.Result<T> return type. I am not sure why yet but I was somehow in my usage of the result trigger the throw. I switched the to the Result type from the Google IO App source and it works now. I guess enabling its use as a return type wasn't the smartest.
try/catch inside viewModelScope.launch {} is not required.
The following code is working fine,
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
class TestCoroutines {
private suspend fun work(): String {
delay(1000)
throw Throwable("Exception From Work")
}
suspend fun invoke(): String {
return try {
withContext(Dispatchers.Default) {
work().let { "Success" }
}
} catch (e: Throwable) {
"Catch Inside:: invoke"
}
}
fun action() {
runBlocking {
val result = invoke()
println(result)
}
}
}
fun main() {
TestCoroutines().action()
}
Please check the entire flow if same exception is being thrown from other places.
Usually I'm returning from my dao suspend function:
#Dao
interface DataDao {
#Query("SELECT * FROM data")
fun getAllData(): List<Data>
}
And handle the call within the repository:
class DataRepository(
private val dataDao: DataDao
) {
fun getAllData(): Flow<DataState> = flow {
val cacheResult = safeDatabaseCall(dispatcher = Dispatchers.IO) { dataDao.getAllData() }
//handle cacheResult, convert to DataState, emit DataState values
}.flowOn(Dispatchers.IO)
}
With generic fun:
suspend fun <T> safeDatabaseCall(
dispatcher: CoroutineDispatcher,
cacheCall: suspend () -> T?
): CacheResult<T?> {
return withContext(dispatcher) {
try {
withTimeout(10000L) {
CacheResult.Success(cacheCall.invoke())
}
} catch (t: Throwable) {
when (t) {
is TimeoutCancellationException -> {
CacheResult.Error("Timeout error")
}
else -> {
CacheResult.Error("Unknown error")
}
}
}
}
}
The problem is that I want return fun getAllData(): Flow<List<Data>> instead of fun getAllData(): List<Data> In order to get immediate updates, But if I'm returning Flow from the Dao, I can't handle the call with safe call and catch errors.
I thought about collecting the data, but if i'm collecting the data the call already done without error handling
Basically I need the cache result return CacheResult<Data> and not CacheResult<Flow<Data>>
How can I solve the problem And make a generic safeDatabaseCall while returning Flow from Dao?
So if I understand correctly you just want to handle the query and return of information safely in a flow. My only question is around the types. I can sorta assume Data DataState and CacheResult are not the same types so I use a "magic" function that converts the intermediary values to the correct one. You will need to adjust accordingly
class DataRepository(
private val dataDao: DataDao
) {
fun getAllData(): Flow<DataState> = flow {
val result = safeDatabaseCall(dispatcher = Dispatchers.IO) {
dataDao.getAllData()
}
// Emit the result
emit(result)
}.catch { t : Throwable ->
// Do our transformation like before
val result = when (t) {
is TimeoutCancellationException -> {
CacheResult.Error("Timeout error")
}
else -> {
CacheResult.Error("Unknown error")
}
}
// And because catch is actually extending a FlowCollector
// We can emit the result in the stream
emit(result)
}.map { cacheResult ->
convertToDataOrDataState(cacheResult)
}
You shouldn't need flowOn with a dispatcher here since the work inside this flow doesn't require thread dispatching
to Dispatcher.IO. The code we are putting in our flow, is purely exception handling and invoking a function. The only place that seems to require any manual dispatch changing is, safeDatabaseCall(). I am not familiar with this function but if it does exist and takes a dispatcher for the result of actualing making the db calls on an IO thread, then all should be good without flowOn. Otherwise you will be switching dispatchers from original dispatcher -> IO and then to IO again. It's not much but the extra no-op context switch doesn't add anything other than confusion later on.
The flow itself traps any upstream issues and you then make them part of the resulting flow