一旦 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.0
,ViewModel
现在有viewModelScope
扩展属性使用SupervisorJob
默认值。并且ViewModel
清除后会自动清除。
【讨论】:
所以你的意思是说我不需要使用 Job() 或 SupervisorJob() 因为我的协程会在 viewmodel 被清除后自动被清除 @AnkitDubey 是的,因为viewModelScope
在内部使用SupervisorJob
。以上是关于一旦 API 失败,协程永远不会调用 API的主要内容,如果未能解决你的问题,请参考以下文章
OsX 中 USB HID API 的回调永远不会被游戏手柄或其他任何东西调用
AFHTTPSessionManager 上的 setDataTaskWillCacheResponseBlock 设置的块永远不会被调用