在 ViewModel 中收集流。是否需要repeatOnLifeCycle?

Tha*_*man 17 android kotlin kotlin-coroutines

到目前为止,我曾经在活动/片段或 ViewModel 中收集我的流,如下所示

活动/片段

lifecycleScope.launch {
    myViewModel.readTokenCredentials().collect { data -> /* do something */ }
}
Run Code Online (Sandbox Code Playgroud)

视图模型

viewModelScope.launch {
    prefsRepo.readTokenCredentials().collect { data -> /* do something */ }
}
Run Code Online (Sandbox Code Playgroud)

现在,谷歌开发人员告诉我们,这不是一种收集流量的安全方法,因为它可能会导致内存泄漏。相反,他们建议将集合包装在lifecycle.repeatOnLifecycle“活动/片段”中以进行流集合。

lifecycleScope.launch {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        myViewModel.readTokenCredentials().collect { data -> /* do something */ }
    }   
}
Run Code Online (Sandbox Code Playgroud)

我的问题是:

为什么在视图模型内收集流时不能使用repeatOnLifecyclewith ?viewModelScope当然,我知道视图模型不具有生命周期意识,但viewModelScope在流收集期间可能不太可能引入内存泄漏?

Ten*_*r04 20

由于 ViewModel 没有重复的生命周期,因此不可能重复生命周期。它启动一次,销毁一次。

我不认为内存泄漏是一个准确的术语来描述当片段在屏幕外时继续收集流时所发生的情况。它只是导致其上游 Flow 无缘无故地继续发射,但发射的项目将被垃圾收集。这简直就是浪费活动。如果您还在收集器中更新 UI,则会出现危险,因为您可能会意外更新屏幕外的视图。

在 ViewModel 中,您同样面临无缘无故地从 Flows 中收集数据的风险。为了避免这种情况,您可以将stateInorshareIn与一个WhileSubscribed值一起使用。然后当下游没有任何收集时就会停止收集。如果您repeatOnLifecycle在活动和片段中使用从这些 SharedFlows 和 StateFlows 收集的数据,那么一切都会得到解决。

例如:

val someFlow = prefsRepo.readTokenCredentials()
    .map { data -> // doSomething }
    .shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 1)
Run Code Online (Sandbox Code Playgroud)

并收集在UI层。如果 UI 没有任何东西可以收集,那么 Flow 最初为何存在?我想不出一个好的反例。ViewModel 旨在准备模型以供查看,而不是执行从未见过的工作。


Con*_*tor 7

总而言之:

在 ViewModel 中,使用stateIn

someFlow.stateIn(
            scope = viewModelScope,
            initialValue = , // set initial value here
            started = SharingStarted.WhileSubscribed(5000)
        )
Run Code Online (Sandbox Code Playgroud)

如果需要处理配置更改,这可能是最好的方法。

同样,如果您愿意将不必要的 LiveData 添加到混合中,实际上可以使用更少的代码来实现,someFlow.asLiveData()也默认为timeoutInMs = 5000. (见底部附加说明)

或者

在 Activity onCreate/Fragment中onCreateView,使用:repeatOnLifecycle。

(以下所有主要结论均以大字体显示)


在看完完整的视频解释(链接到“Android UI 中的流程”的相关部分)配套文档后,我能够澄清许多概念。我建议大家观看并阅读它,如有必要,请暂停并多次阅读。除了最初的问题之外,以下是我的笔记:

有两件事需要考虑。

  1. 第一个是当应用程序在后台时不浪费资源,并且
  2. 第二个是关于配置更改

首先,UI 应仅在需要时使用生命周期感知替代方案收集项目。具体来说,这意味着:

在 ViewModel 中:使用Flow<T>.asLiveData().

asLiveData 流运算符将流转换为仅当 UI 在屏幕上可见时观察项目的实时数据。这种转换是我们可以在视图模型类中完成的。在 UI 中,我们只是像往常一样使用 LiveData。

这种方式利用了LiveData的固有属性,但是引入了LiveData作为另一种技术,显得并不优雅。

在 Activity 中onCreate,使用repeatOnLifecycle(Lifecycle.State.STARTED)

这是从 UI 层收集流量的推荐方法。例子:

class LatestNewsActivity : AppCompatActivity() {
    private val latestNewsViewModel = // getViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Start a coroutine in the lifecycle scope
        lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // Note that this happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                latestNewsViewModel.uiState.collect { uiState ->
                    // New value received
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

该 API 具有生命周期感知能力,因为当生命周期到达该步骤时,它会自动启动一个新的协程,并向其传递一个块。然后,当生命周期低于该状态时,正在进行的协程将被取消。在块内,我们可以调用collect,就像我们在协程的上下文中一样。由于repeatOnLifecycle是一个挂起函数,因此也需要在协程中调用。当您处于一项活动中时,我们可以使用生命周期作用域来启动一项活动。正如您所看到的,最佳实践是在生命周期初始化时调用此函数,例如在此 Activity 的 onCreate 中。RepeatOnLifecycle 的可重新启动行为会自动为您考虑 UI 生命周期。

请注意,如果您需要从多个流中收集,请参阅视频/文档了解详细信息。

当只需要收集一个流时,另一种选择是flowWithLifecycle(lifecycle, State.STARTED)

请观看视频,详细了解为什么旧 API ( launchlaunchWhenX) 如果不手动停止可能会造成浪费/危险。

在Fragment中,可以使用viewLifecycleOwner.lifecycleScope.launch和。viewLifecycleOwner.repeatOnLifecycle


其次,处理配置更改。具体来说,当活动/片段经历其生命周期时,ViewModel 将保留其自己的生命周期。(视频中有更多详细信息)在这种情况下我们如何处理 ViewModel?

解决方案是在LiveData中使用StateFlow。

这与 LiveData 非常相似,但一个关键的区别是它对初始值的要求是固执己见的。

有两种实现 StateFlow 的方法。

第一个不推荐的方法是使用(支持)MutableStateFlow 并手动更新它stateFlow.value = newValue但这并不理想,因为这不是一个“反应式”解决方案:当这个 StateFlow 没有 UI 订阅者时,它仍然会更新。

推荐的方法是使用stateIn中间运算符。

val result: StateFlow<Result<T>> = someFlow
    .stateIn(
            scope = viewModelScope,
            initialValue = , // set initial value here
            started = SharingStarted.WhileSubscribed(5000)
        )
Run Code Online (Sandbox Code Playgroud)

5000 ms 听起来有些随意:它基本上意味着如果没有订阅者,它将在 5000 ms 后停止流,但如果有配置更改(例如轮换),则流会在 5000 ms 内恢复,因此相同的流是保持活力。

关于 StateFlow 和 LiveData 之间差异的附加说明:

但请注意,StateFlow 和 LiveData 的行为确实不同:

  • StateFlow 需要将初始状态传递给构造函数,而 LiveData 则不需要。
  • 当视图进入 STOPPED 状态时,LiveData.observe() 会自动取消注册使用者,而从 StateFlow 或任何其他流收集不会自动停止收集。要实现相同的行为,您需要从 Lifecycle.repeatOnLifecycle 块收集流。