将多个挂起函数映射到单个 LiveData

Map multiple suspend functions to single LiveData

我刚开始工作的公司使用所谓的 Navigator,我现在将其解释为无状态 ViewModel。我的导航器收到一些用例,每个用例都包含 1 个挂起函数。任何这些用例的结果都可能以单个 LiveData 结束。 Navigator 没有协程作用域,因此我使用 fetchValue().

将作用域暂停的责任传递给 Fragment

项目中的大多数当前代码在数据层中都有 LiveData,我尽量不这样做。正因为如此,他们的 livedata 是从 view 到 dao 的 linked。

我的简化版类:

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() 是 link LiveData 挂起函数的好方法吗? 能不能漏还有其他问题吗?

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

  • 我在数据层只使用协程(和流)的主要原因, 是 'because Google said so'。什么是更好的理由?

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

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

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

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

正如我所说,没有严格的规则,这取决于特定的用例。此外,我发现在视图模型中使用 Flow 非常有用 - 它提供了很多运算符并使代码简洁明了。但是我借助 asLiveData() 之类的扩展将其转换为 LiveData,并将此 LiveData 公开给 UI。通过这种方式,我从这两个词中得到了最好的结果——LiveData 在重新配置之间捕获价值,Flow 使视图模型的代码漂亮干净。 您也可以经常使用最新的 StateFlowSharedFlow 它们也可以帮助克服 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
    }
}

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

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

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