在 Android 中,无法处理从 Clean Architecture 中的用例抛出的异常

In Android, can't handle exception thrown from Use-case in Clean Architecture

我想在我最近的应用程序中实施 Clean Architecture。但是我在用例中遇到了一些错误处理的麻烦。尽管成功处理了其他基于库的异常,但我无法处理从 Usecase class 抛出的异常。这意味着,当 Firebase 抛出任何异常时,它都会被成功处理并在 Toast 中显示异常文本。但是当我从 Usecase class 中抛出任何异常时,比如

Please fill both field

Please give email

Please give password

然后应用程序崩溃了。那么如何处理用例异常 class 并在 Toast 中显示文本?

这是我试过的代码。

FirebaseAuthRepository.kt

interface FirebaseAuthRepository {
    suspend fun register(email: String, password: String): Flow<User>
}

FirebaseAuthRepositoryImpl.kt

class FirebaseAuthRepositoryImpl @Inject constructor(
    private val firebaseAuthDataSource: FirebaseAuthDataSource,
    private val firebaseAuthMapper: FirebaseAuthMapper
) : FirebaseAuthRepository {

    override suspend fun register(email: String, password: String): Flow<User> {
        return flow {
            emit(firebaseAuthDataSource.register(email, password))
        }.map {
            firebaseAuthMapper.toUser(it)
        }
    }    
}

FirebaseAuthDataSource.kt

class FirebaseAuthDataSource @Inject constructor(private val firebaseAuth: FirebaseAuth) {

    suspend fun register(email: String, password: String): FirebaseUser? {
        val authResult: AuthResult = firebaseAuth.createUserWithEmailAndPassword(email, password).await()
        return authResult.user
    }
}

RegisterUseCase.kt

class RegisterUseCase @Inject constructor(
    private val firebaseAuthRepository: FirebaseAuthRepository
) {

    suspend fun execute(email: String, password: String): Flow<User> {
        if (email.trim().isEmpty() && password.trim().isEmpty()) {
            throw Exception("Please fill both field")
        } else {
            if (email.trim().isEmpty()) throw Exception("Please give email")
            if (password.trim().isEmpty()) throw Exception("Please give password")
        }

        return firebaseAuthRepository.register(email, password)
    }

}

LoginViewModel.kt

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val registerUseCase: RegisterUseCase
) : BaseViewModel() {
    val register: MutableStateFlow<Resource<User>> = MutableStateFlow(Loading(true))

    fun register(email: String, password: String) {
        viewModelScope.launch {
            loginUseCase.execute(email, password)
                .flowOn(Dispatchers.IO)
                .onStart { }
                .onCompletion { register.value = Loading(false) }
                .catch { register.value = Error(it) }
                .collectLatest { register.value = Success(it) }
        }
    }
}

Resource.kt

sealed class Resource<out T> {
    data class Loading(val isLoading: Boolean) : Resource<Nothing>()
    data class Success<T>(val item: T) : Resource<T>()
    data class Error(val throwable: Throwable) : Resource<Nothing>()
}

RegistrationFragment.kt

@AndroidEntryPoint
class RegistrationFragment : BaseFragment<FragmentRegisterBinding>() {

    private val viewModel: RegistrationViewModel by viewModels()

    override fun getViewBinding(
        inflater: LayoutInflater,
        container: ViewGroup?
    ): FragmentRegisterBinding {
        return FragmentRegisterBinding.inflate(inflater, container, false)
    }

    override fun configureViews() {
        binding?.registerButton?.setOnClickListener {
            viewModel.register(
                binding?.mailEditText?.value() ?: "",
                binding?.passwordEditText?.value() ?: ""
            )
        }
    }

    override fun bindWithViewModel() {
        lifecycleScope.launch {
            viewModel.registration.collectLatest {
                when(it) {
                    is Loading -> {
                        if (it.isLoading) {

                        } else {

                        }
                    }

                    is Success -> {
                        findNavController().navigate(R.id.home_fragment)
                    }

                    is Error -> {
                        requireContext().toast(it.throwable.message ?: "")
                    }
                }
            }
        }
    }

}

使用如下所示的 CoroutineExceptionHandler 处理程序(您可以将其放入 BaseViewModel)

val handler = CoroutineExceptionHandler { _, exception ->
    run {
        toast.value = exception.message
        exception.printStackTrace()
        Log.e(TAG, "Caught $exception")
    }
}

然后将 handler 添加到您的 viewModel 中 viewModelScope.launch

fun register(email: String, password: String) {
    viewModelScope.launch(handler) {
        loginUseCase.execute(email, password)
            .flowOn(Dispatchers.IO)
            .onStart { }
            .onCompletion { register.value = Loading(false) }
            .catch { register.value = Error(it) }
            .collectLatest { register.value = Success(it) }
    }
}

我终于解决了这个问题。非常感谢@Rifan vai 的回答。代码在这里,我试过了。

BaseViewmodel.kt

abstract class BaseViewModel : ViewModel() {

    fun handleException(action: (Throwable)->Unit): CoroutineExceptionHandler {
        return CoroutineExceptionHandler { _, exception ->
            action(exception)
        }
    }
}

LoginViewModel.kt

@HiltViewModel
class LoginViewModel @Inject constructor(
    private val loginUseCase: LoginUseCase
) : BaseViewModel() {
    val login: MutableStateFlow<Resource<User>> = MutableStateFlow(Loading(true))

    fun login(email: String, password: String) {
        viewModelScope.launch(handleException { login.value = Error(it) }) {
            loginUseCase.execute(email, password)
                .flowOn(Dispatchers.IO)
                .onCompletion { login.value = Loading(false) }
                .collectLatest { login.value = Success(it) }
        }
    }
}