一旦 API 失败,协程永远不会调用 API

Posted

技术标签:

【中文标题】一旦 API 失败,协程永远不会调用 API【英文标题】:Coroutine never calls API after once API is failed 【发布时间】:2020-03-22 15:41:40 【问题描述】:

在我的视图模型中,我有一个使用协程调用 API 的函数。

fun loadPosts()

    GlobalScope.launch(coroutineContext)
        changeState(load = true)
        val list= withContext(Dispatchers.IO) 
            apiService.getProfile(AUTH)
        
        changeState(false,false,null)
        showResult(list)
    

每次我点击一个按钮,这个函数都会被触发,API 被调用并且我得到有效的响应。但是一旦我的 api 得到 500 或 Http 401 Unauthorized 之类的异常,然后当我再次点击按钮时,协程永远不会被调用,并且似乎它从缓存中返回了再次错误消息。

对于一个用例:

我点击按钮 -> Api 被调用 -> 得到成功响应

我再次点击 -> Api 被调用 -> 得到成功响应

我从手机上断开了互联网

我点击了按钮 -​​> 调用了 Api -> 出现了类似 ConnectionError 的异常

我将手机连接到互联网

我点击了按钮 -​​> Api 未被调用 -> 出现了类似 ConnectionError 的异常

现在即使我的手机有有效的互联网连接,我按下按钮,而不是调用 api 并等待响应,它一次又一次地给我之前失败的响应。

之前我使用的是 Rxjava,但我没有遇到任何此类问题。我是协程的新手,所以如果有人有任何建议,欢迎您

【问题讨论】:

您在CoroutineScope() 通话中放置的coroutineContext 中有什么内容?我怀疑你在上下文中有一个常规的Job 实例。 coroutineContext 在我的 BaseViewModel 中定义,例如抽象类 BaseViewModel : ViewModel(), CoroutineScope private val job: Job= Job() override val coroutineContext: CoroutineContext get() = Dispatchers.Main +工作 + 处理程序 _ _ _ _ 你使用SupervisorJob吗?你应该使用SupervisorJob 来避免子协程的错误传播。否则,当孩子出现异常时,您的根作业将被取消。 主管职位?这个对我来说是新的吧。让我检查一下。 @Choim 非常感谢。似乎它现在工作正常。我刚刚将 Job() 替换为 SuperivsorJob() 并且它现在工作正常。不知道背后的魔法还开心。 【参考方案1】:

每当您使用诸如launch 之类的协程构建器启动协程时,您都需要在给定的CoroutineScope 中启动它——这是由defined as an extension on CoroutineScope 函数强制执行的。这个范围包含一个CoroutineContext,它将定义协程的执行方式。

根据上面的评论,我假设您使用的设置大致如下:

abstract class BaseViewModel : ViewModel(), CoroutineScope  
    private val job: Job = Job()
    override val coroutineContext: CoroutineContext 
        get() = Dispatchers.Main + job

    override fun onCleared() 
        coroutineContext.cancel()
    

通过使用GlobalScope.launch(coroutineContext),您实际上用上下文参数覆盖了GlobalScope 提供的所有内容。另外,由于您的 ViewModel 本身已经是一个范围,因此您首先不需要在 GlobalScope 中添加 launch。你可以简单地在ViewModel 中写下launch,没有指定范围(本质上是this.launch )并且没有传递给它的上下文,因为无论如何它都会从范围中获取一个。

另一个问题是您使用常规的Job 作为CoroutineContext 的一部分。这个Job 成为您启动的每个协程的父协程,并且每当子协程失败(例如网络错误)时,父协程 Job 也会被取消 - 这意味着您尝试启动的任何其他子协程也将立即失败,因为您无法在已经失败的Job 下开始新的孩子(有关更多详细信息,请参阅Job documentation)。

为避免这种情况,您可以改用SupervisorJob,它还可以将您的协程作为子协程组合在一起,并在ViewModel 被清除时取消它们,但如果其中一个子协程失败,则不会被取消。


所以总结一下要在代码级别快速进行的修复:

ViewModel 中使用SupervisorJob(当你在那里时,只需创建一次这个组合的CoroutineContext,直接分配它,而不是将它放在getter 中):

override val coroutineContext: CoroutineContext = Dispatchers.Main + SupervisorJob()

ViewModel 而不是GlobalScope 中定义的范围内启动您的协程:

abstract class BaseViewModel : ViewModel(), CoroutineScope  
     // ...

    fun load() 
        launch  // equivalent to this.launch, because `this` is a CoroutineScope
            // do loading
        
    


您可能还想考虑让您的ViewModel 包含CoroutineScope 而不是实现接口本身,as described here 和discussed here。


关于这个主题有很多阅读要做,这里有一些我通常推荐的文章:

https://medium.com/@elizarov/coroutine-context-and-scope-c8b255d59055 https://medium.com/@elizarov/the-reason-to-avoid-globalscope-835337445abc https://proandroiddev.com/demystifying-coroutinecontext-1ce5b68407ad

...以及 Roman Elizarov 在他的博客上的所有其他内容,真的 :)

【讨论】:

我认为不再推荐实现CoroutineScope 的长手风格,by MainScope 的新方法具有相同的语义,但样板更少。另一方面,组合风格与继承风格各有优势,都不是全能的赢家。【参考方案2】:

还有一个选择。

如果你使用androidx.lifecycle:lifecycle-extensions:2.1.0ViewModel 现在有viewModelScope 扩展属性使用SupervisorJob 默认值。并且ViewModel清除后会自动清除。

【讨论】:

所以你的意思是说我不需要使用 Job() 或 SupervisorJob() 因为我的协程会在 viewmodel 被清除后自动被清除 @AnkitDubey 是的,因为viewModelScope 在内部使用SupervisorJob

以上是关于一旦 API 失败,协程永远不会调用 API的主要内容,如果未能解决你的问题,请参考以下文章

OsX 中 USB HID API 的回调永远不会被游戏手柄或其他任何东西调用

节点递归承诺永远不会退出

AFHTTPSessionManager 上的 setDataTaskWillCacheResponseBlock 设置的块永远不会被调用

进行多个协程 API 调用并等待所有调用

onRequestPermissionsResult 永远不会在片段中被调用 [重复]

如果在函数内,则不会从mongodb检索API调用