将多个挂起函数映射到单个实时数据



我刚开始工作的公司使用所谓的导航器,我现在将其解释为无状态的ViewModel。我的导航器收到一些用例,每个用例包含 1 个挂起函数。任何这些用例的结果都可能最终出现在单个 LiveData 中。导航器没有协程作用域,因此我使用fetchValue()将范围暂停的责任传递给片段。

项目中的大多数当前代码在数据层中都有 LiveData,我尽量不这样做。正因为如此,他们的实时数据从视图链接到 dao。

我的简化类:

class MyFeatureNavigator(
getUrl1: getUrl1UseCase,
getUrl1: getUrl1UseCase
) {
val url = MediatorLiveData<String>()
fun goToUrl1() {
url.fetchValue { getUrl1() }
}
fun goToUrl2() {
url.fetchValue { getUrl2() }
}
fun <T> MediatorLiveData<T>.fetchValue(provideValue: suspend () -> T) {
val liveData = liveData { emit(provideValue()) }
addSource(liveData) {
removeSource(liveData)
value = it
}
}
}
class MyFeatureFragment : Fragment {
val viewModel: MyFeatureViewModel by viewModel()
val navigator: MyFeatureNavigator by inject()
fun onViewCreated() {
button.setOnClickListener { navigator.goToUrl1() }
navigator.url.observe(viewLifecycleOwner, Observer { url ->
openUrl(url)
})
}
}

我的两个问题:

  • fetchValue()是将挂起函数链接到 LiveData 的好方法吗?会不会漏?还有其他问题吗?
  • 我只在数据层使用协程(和流)的主要原因是"因为谷歌是这么说的"。有什么更好的理由呢?并且:与项目和当前的良好编码实践保持一致的最佳权衡是什么?
  • fetchValue() 是将挂起函数链接到 LiveData 的好方法吗? 会不会漏?还有其他问题吗?

一般来说,它应该有效。在添加新的 url 之前,您可能应该删除MediatorLiveData的先前源,否则,如果您连续收到两次对fetchValue的调用,则获取第一个 url 的速度可能会较慢,因此它会稍后出现并获胜。 我没有看到任何其他正确性问题,但这段代码非常复杂,创建了几个中间对象,通常难以阅读。

  • 我只在数据层使用协程(和流)的主要原因, 是"因为谷歌是这么说的"。有什么更好的理由呢?

Google提供了许多有用的扩展来在UI层中使用协程,例如,看看这个页面。所以很明显,他们鼓励人们使用它。

可能您的意思是建议使用LiveData而不是 UI 层中的Flow。这不是一个严格的规则,它有一个原因:LiveData是一个价值持有者,它保持其价值并立即提供给新订阅者,而无需做任何工作。这在 UI/ViewModel 层中特别有用 - 当发生配置更改并重新创建活动/片段时,新创建的活动/片段使用相同的视图模型,订阅相同的LiveData并免费接收值。

同时Flow是"冷"的,如果从视图模型公开流,则每次重新配置都将触发一个新的流集合,并且流将从头开始执行。

因此,例如,如果您从数据库或网络获取数据,LiveData只会向新订阅者提供最后一个值,Flow将再次执行昂贵的数据库/网络操作。

所以正如我所说,没有严格的规则,这取决于特定的用例。此外,我发现在视图模型中使用Flow非常有用 - 它提供了很多运算符并使代码干净简洁。但是,我借助asLiveData()等扩展将其转换为LiveData,并将此LiveData公开给UI。通过这种方式,我从这两个词中获得最佳效果 -LiveData重新配置和Flow之间捕获价值,并使视图模型的代码变得漂亮而干净。 您也可以使用最新的StateFlow,并且经常SharedFlow它们还可以帮助克服UI层中提到的Flow问题。

回到你的代码,我会像这样实现它:

class MyFeatureNavigator(
getUrl1: getUrl1UseCase,
getUrl1: getUrl1UseCase
) {
private val currentUseCase = MutableStateFlow<UseCase?>(null)
val url = currentUseCase.filterNotNull().mapLatest { source -> source.getData()}.asLiveData()

fun goToUrl1() {
currentUseCase.value = getUrl1
}
fun goToUrl2() {
currentUseCase.value = getUrl2
}
}

这样就没有竞争条件需要关心,代码是干净的。

  • 并且:与项目保持一致的最佳权衡是什么 以及当前的良好编码实践?

这是一个有争议的问题,它应该主要是团队的决定。在我参与的大多数项目中,我们采用了这样的规则:在修复错误,维护现有代码时,应该遵循相同的风格。在进行大型重构/实现新功能时,应该使用团队采用的最新实践。

最新更新