当所有协程都已用 CouroutineExceptionHandler 包装时,如何找出“作业被取消”异常的来源?

2023-11-26

我读了所有kotlinx 用户界面文档并实现一个 ScopedActivity ,就像那里描述的那样(参见下面的代码)。

在我的 ScopedActivity 实现中,我还添加了一个 CouroutineExceptionHandler,尽管我将异常处理程序传递给了所有协程,但我的用户遇到了崩溃,并且我在堆栈跟踪中获得的唯一信息是“作业已取消”。

我搜索了几天,但没有找到解决方案,我的用户仍然随机崩溃,但我不明白为什么......

这是我的 ScopedActivity 实现

abstract class ScopedActivity : BaseActivity(), CoroutineScope by MainScope() {

    val errorHandler by lazy { CoroutineExceptionHandler { _, throwable -> onError(throwable) } }

    open fun onError(e: Throwable? = null) {
        e ?: return
        Timber.i(e)
    }

    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}

这是实现它的活动的示例:

class ManageBalanceActivity : ScopedActivity() {

    @Inject
    lateinit var viewModel: ManageBalanceViewModel

    private var stateJob: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_manage_balance)
        AndroidInjection.inject(this)

        init()
    }

    private fun init() {
        SceneManager.create(
            SceneCreator.with(this)
                .add(Scene.MAIN, R.id.activity_manage_balance_topup_view)
                .add(Scene.MAIN, R.id.activity_manage_balance_topup_bt)
                .add(Scene.SPINNER, R.id.activity_manage_balance_spinner)
                .add(Scene.SPINNER, R.id.activity_manage_balance_info_text)
                .add(Scene.PLACEHOLDER, R.id.activity_manage_balance_error_text)
                .first(Scene.SPINNER)
        )

        // Setting some onClickListeners ...
        bindViewModel()
    }

    private fun bindViewModel() {
        showProgress()
        stateJob = launch(errorHandler) {
            viewModel.state.collect { manageState(it) }
        }
    }

    private fun manageState(state: ManageBalanceState) = when (state) {
        is ManageBalanceState.NoPaymentMethod -> viewModel.navigateToManagePaymentMethod()
        is ManageBalanceState.HasPaymentMethod -> onPaymentMethodAvailable(state.balance)
    }

    private fun onPaymentMethodAvailable(balance: Cash) {
        toolbarTitle.text = formatCost(balance)
        activity_manage_balance_topup_view.currency = balance.currency
        SceneManager.scene(this, Scene.MAIN)
    }

    override fun onError(e: Throwable?) {
        super.onError(e)
        when (e) {
            is NotLoggedInException -> loadErrorScene(R.string.error_pls_signin)
            else -> loadErrorScene()
        }
    }

    private fun loadErrorScene(@StringRes textRes: Int = R.string.generic_error) {

   activity_manage_balance_error_text.setOnClickListener(this::reload)
        SceneManager.scene(this, Scene.PLACEHOLDER)
    }

    private fun reload(v: View) {
        v.setOnClickListener(null)
        stateJob.cancelIfPossible()
        bindViewModel()
    }

    private fun showProgress(@StringRes textRes: Int = R.string.please_wait_no_dot) {
        activity_manage_balance_info_text.setText(textRes)
        SceneManager.scene(this, Scene.SPINNER)
    }

    override fun onDestroy() {
        super.onDestroy()
        SceneManager.release(this)
    }
}
fun Job?.cancelIfPossible() {
    if (this?.isActive == true) cancel()
}

这是 ViewModel

class ManageBalanceViewModel @Inject constructor(
    private val userGateway: UserGateway,
    private val paymentGateway: PaymentGateway,
    private val managePaymentMethodNavigator: ManagePaymentMethodNavigator
) {

    val state: Flow<ManageBalanceState>
        get() = paymentGateway.collectSelectedPaymentMethod()
            .combine(userGateway.collectLoggedUser()) { paymentMethod, user ->
                when (paymentMethod) {
                    null -> ManageBalanceState.NoPaymentMethod
                    else -> ManageBalanceState.HasPaymentMethod(Cash(user.creditBalance.toInt(), user.currency!!))
                }
            }
            .flowOn(Dispatchers.Default)

    // The navigator just do a startActivity with a clear task
    fun navigateToManagePaymentMethod() = managePaymentMethodNavigator.navigate(true)
}

该问题来自 Kotlin Flow 尝试在取消后发出,以下是我为消除生产中发生的崩溃而创建的扩展:

/**
 * Check if the channel is not closed and try to emit a value, catching [CancellationException] if the corresponding
 * has been cancelled. This extension is used in call callbackFlow.
 */
@ExperimentalCoroutinesApi
fun <E> SendChannel<E>.safeOffer(value: E): Boolean {
    if (isClosedForSend) return false
    return try {
        offer(value)
    } catch (e: CancellationException) {
        false
    }
}

/**
 * Terminal flow operator that collects the given flow with a provided [action] and catch [CancellationException]
 */
suspend inline fun <T> Flow<T>.safeCollect(crossinline action: suspend (value: T) -> Unit): Unit =
    collect { value ->
        try {
            action(value)
        } catch (e: CancellationException) {
            // Do nothing
        }
    }

/**
 * Terminal flow operator that [launches][launch] the [collection][collect] of the given flow in the [scope] and catch
 * [CancellationException]
 * It is a shorthand for `scope.launch { flow.safeCollect {} }`.
 */
fun <T> Flow<T>.safeLaunchIn(scope: CoroutineScope) = scope.launch {
    [email protected] { /* Do nothing */ }
}

希望能帮助到你

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

当所有协程都已用 CouroutineExceptionHandler 包装时,如何找出“作业被取消”异常的来源? 的相关文章

随机推荐