Как определить, откуда возникает исключение «Работа была отменена», когда все ваши сопрограммы уже обернуты CouroutineExceptionHandler? - PullRequest
1 голос
/ 06 ноября 2019

Я прочитал все документы 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)
}

1 Ответ

0 голосов
/ 08 ноября 2019

Скорее всего, проблемы возникают из-за того, что вы передаете свой обработчик исключений сопрограммы (назовем его CEH) непосредственно в блоки запуска. Эти блоки запуска создают новые задания (важные - обычные задания, а не супервизоры), которые становятся дочерними элементами задания в области действия (MainScope в вашей области действия).

Обычное задание отменяет всех своих потомков и самого себя,если кто-либо из его детей выдвигает исключение. CEH не помешает этому поведению. Он получит эти исключения и сделает то, что ему было сказано, но все равно не предотвратит отмену задания в области действия и всех его дочерних элементов. Самое главное, что он будет распространять исключение до иерархии. TLDR - сбой не будет обработан.

Чтобы ваш CEH мог выполнять свою работу, вам необходимо установить его в контексте с SuperVisorJob (или NonCancellable). SupervisorJob предполагает, что вы контролируете исключения в его области, поэтому он не отменяет себя или своих дочерних элементов при возникновении исключения (однако, если исключение вообще не обрабатывается, оно все равно будет распространяться по иерархии).

Например, в вашей области действия ScopedActivity:

abstract class ScopedActivity : BaseActivity(), CoroutineScope {

override val coroutineContext = Dispatchers.Main + SupervisorJob() + CoroutineExceptionHandler { _, error -> 
...
    }

Если вы действительно хотите, вы можете установить CEH глубже в иерархии сопрограмм. Тем не менее, это будет выглядеть неуклюже и не рекомендуется:

launch {
    val supervisedJob = SupervisorJob(coroutineContext[Job])
    launch(supervisedJob + CEH) { 
        throw Exception() 
    }
    yield()
    println("I am still alive, exception was catched by CEH")
}

Вышеуказанная практика может оказаться полезной, если вы хотите запустить какой-нибудь не подлежащий отмене побочный эффект:

launch(NonCancellable + CEH) {
        throw Exception()
    }
...