如何解决Kotlin Flow:当 Fragment 变得不可见时取消订阅 SharedFlow
我读过类似的主题,但找不到正确的答案:
- How to end / close a MutableSharedFlow?
- Kotlin Flow: How to unsubscribe/stop
- StateFlow and SharedFlow. Making cold flows hot using shareIn - Android 文档
- Introduce SharedFlow - 由 Roman Elizarov 发起的 GH 讨论
在我的 Repository
课上,我有一个冷的 Flow
我想分享给 2 Presenters
/ViewModels
所以我的选择是使用 shareIn
运算符.
让我们看一下 Android 文档的示例:
val latestNews: Flow<List<ArticleHeadline>> = flow {
...
}.shareIn(
externalScope,// e.g. CoroutineScope(Dispatchers.IO)?
replay = 1,started = SharingStarted.WhileSubscribed()
)
文档对 externalScope
参数的建议:
一个用于共享流的 CoroutineScope。这个范围应该比任何消费者活得更久,以在需要时保持共享流的活动。
然而,寻找关于如何停止订阅 Flow
的答案,第二个链接中投票最多的答案说:
解决方案不是取消流程,而是取消流程的范围。
对我来说,这些答案在 SharedFlow
的情况下是矛盾的。不幸的是,即使在其 Presenter
被调用后,我的 ViewModel
/onCleared
仍然收到最新数据。
如何防止?这是我如何在 Flow
/Presenter
中使用此 ViewModel
的示例:
fun doSomethingUseful(): Flow<OtherModel> {
return repository.latestNews.map(OtherModel)
如果这可能有帮助,我正在使用 MVI 架构,因此 doSomethingUseful
会对用户创建的某些意图做出反应。
解决方法
我试图提供一个带有相关评论的最小示例。如前所述,SharedFlow 的工作方式与 RxJava 中的 destroyed()
非常相似。上游将仅被订阅一次,这意味着仅对冷上游流进行一次计算。您的存储库什么都不做,因为它是一个冷流,在 ConnectableObservable
订阅之前永远不会“收集”,因此它没有范围。
使用过 RxJava 和 Flow,有很多相似之处。创建 SharedFlow
和 Flow
接口似乎几乎没有必要,如果扩展基本 Reactive Streams 接口,可能会使开发人员更容易过渡 - 但我不知道根本原因 -也许他们希望通过新的 api 获得更大的灵活性,或者从另一个响应式流实现(如 Java 9 实现和 RxJava)中脱颖而出。
Collector
,
感谢 Mark Keen 的评论和帖子,我想我设法得到了满意的结果。
我知道 shareIn
参数中定义的范围不必与我的使用者操作的范围相同。将 BasePresenter
/BaseViewModel
中的范围从 CoroutineScope
更改为 viewModelScope
似乎解决了主要问题。您甚至不需要手动取消此范围,如 Android docs 中所定义:
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
请记住,默认的 viewModelScope
调度程序是 Main
,这并不明显,它可能不是您想要的!要更改调度员,请使用 viewModelScope.launch(YourDispatcher)
。
更重要的是,我的热 SharedFlow
是从另一个在 Flow
回调 API(基于 callbackFlow
API - 这很复杂)上创建的冷 Channels
转换而来的...)
将集合范围更改为 viewModelScope
后,从该 API 发出新数据时出现 ChildCancelledException: Child of the scoped flow was cancelled
异常。这个问题在 GitHub 上的两个问题中都有详细记录:
- kotlinx.coroutines.flow.internal.ChildCancelledException: Child of the scoped flow was cancelled
- SendChannel.offer should never throw
如前所述,使用 offer
和 send
的发射之间存在细微差别:
offer 用于非挂起上下文,而 send 用于挂起上下文。
不幸的是,就传播的异常而言,offer 是非对称发送的(来自 send 的 CancellationException 通常被忽略,而在 nom-suspending 上下文中来自 offer 的 CancellationException 则不会)。
我们希望通过 offerOrClosed 或更改优惠语义在 #974 中修复它
至于 1.4.2 的 Kotlin Coroutines,#974 还没有修复 - 我希望它会在最近的将来避免意外的 CancellationException
。
最后,我建议在 started
运算符中使用 shareIn
参数。在所有这些更改之后,我不得不在我的用例中从 WhileSubscribed()
更改为 Lazily
。
如果我发现任何新信息,我会更新这篇文章。希望我的研究能节省一些人的时间。
,使用共享流。在下面的示例中,我从一个片段发出值并在另一个片段上收集它。
视图模型:
class MenuOptionsViewModel : ViewModel() {
private val _option = MutableSharedFlow<String>()
val option = _option.asSharedFlow()
suspend fun setOption(o : String){
_option.emit(o)
}
}
片段发射值:
class BottomSheetOptionsFragment : BottomSheetDialogFragment(),KodeinAware{
override fun onViewCreated(view: View,savedInstanceState: Bundle?) {
super.onViewCreated(view,savedInstanceState)
menuViewModel = activity?.run {
ViewModelProviders.of(this).get(MenuOptionsViewModel::class.java)
} ?: throw Exception("Invalid Activity")
listViewOptions.adapter = ArrayAdapter<String>(
requireContext(),R.layout.menu_text_item,options
)
listViewOptions.setOnItemClickListener { adapterView,view,i,l ->
val entry: String = listViewOptions.getAdapter().getItem(i) as String
// here we are emitting values
GlobalScope.launch { menuViewModel.setOption(entry) }
Log.d(TAG,"emitting flow $entry")
dismiss()
}
}
}
片段收集值:
class DetailFragment : BaseFragment(),View.OnClickListener,KodeinAware,OnItemClickListener {
override fun onViewCreated(view: View,savedInstanceState: Bundle?) {
super.onViewCreated(view,savedInstanceState)
menuViewModel = activity?.run {
ViewModelProviders.of(this).get(MenuOptionsViewModel::class.java)
} ?: throw Exception("Invalid Activity")
// collecting values
lifecycleScope.launchWhenStarted {
menuViewModel.option.collect {
Log.d(TAG,"collecting flow $it")
}
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。