【问题标题】:Map multiple suspend functions to single LiveData将多个挂起函数映射到单个 LiveData
【发布时间】:2020-11-20 20:49:31
【问题描述】:

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

项目中的大多数当前代码在数据层中都有 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 的好方法吗?会不会漏?还有其他问题吗?
  • 我只在数据层使用协程(和流)的主要原因是“因为 Google 这么说”。这有什么更好的理由?并且:在与项目和当前良好的编码实践保持一致方面,最佳权衡是什么?

【问题讨论】:

    标签: android kotlin android-livedata kotlin-coroutines


    【解决方案1】:
    • fetchValue() 是将挂起函数链接到 LiveData 的好方法吗? 会不会漏?还有其他问题吗?

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

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

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

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

    同时Flow 是“冷”的,如果您从视图模型中公开流程,每次重新配置都会触发新的流程集合,流程将从头开始执行。

    例如如果您从 db 或 network 获取数据,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
        }
    }
    

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

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

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

    【讨论】:

      猜你喜欢
      • 2018-01-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-23
      • 1970-01-01
      • 2015-01-07
      • 2013-08-20
      相关资源
      最近更新 更多