0

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: 
Dan Lee
  • 25
  • 6

1 Answers1

0

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) }
    }
}
trenthudy
  • 68
  • 6
  • Okay, got it now, instead of chaining it with `map` i chain it with `flatMap` to catch the api error, thanks! – Dan Lee Jun 03 '20 at 16:29