从 ViewModel 观察 LiveData

我有一个单独的类,处理数据获取(特别是 Firebase) ,通常从中返回 LiveData 对象并异步更新它们。现在,我希望将返回的数据存储在 ViewModel 中,但问题是,为了获得所述值,我需要观察从数据获取类返回的 LiveData 对象。Observer 方法需要一个 LifcycleOwner 对象作为第一个参数,但是我的 ViewModel 中显然没有这个参数,而且我知道我不应该在 ViewModel 中保留对活动/片段的引用。我该怎么办?

111729 次浏览

在谷歌开发者 Jose Alcérreca 的 这篇博文中,我们建议在这个例子中使用转换(参见“存储库中的 LiveData”段落) ,因为 ViewModel不应该包含任何与 View相关的引用(活动,上下文等) ,因为它使得它很难测试。

ViewModel文档中

然而,ViewModel 对象绝不能观察对生命周期感知的可观察对象(例如 LiveData 对象)的更改。

另一种方法是让数据实现 RxJava 而不是 LiveData,这样它就不会有生命周期感知的好处。

在 google 的 Todo-mvvm-live-kotlin示例中,它在 ViewModel 中使用了一个没有 LiveData 的回调。

我猜想,如果您想遵循作为生命周期软件的整体思想,我们需要在活动/片段中移动观察代码。否则,我们可以在 ViewModel 中使用回调或 RxJava。

Another compromise is implement MediatorLiveData (or Transformations) and observe (put your logic here) in ViewModel. Notice MediatorLiveData observer won't trigger (same as Transformations) unless it's observed in Activity/Fragment. What we do is we put a blank observe in Activity/Fragment, where the real work is actually done in ViewModel.

// ViewModel
fun start(id : Long) : LiveData<User>? {
val liveData = MediatorLiveData<User>()
liveData.addSource(dataSource.getById(id), Observer {
if (it != null) {
// put your logic here
}
})
}


// Activity/Fragment
viewModel.start(id)?.observe(this, Observer {
// blank observe here
})

附注: 我读了 ViewModel 和 LiveData: 模式 + 反模式的建议,转换。我认为除非 LiveData 被观察到(这可能需要在 Activity/Fragment 中完成) ,否则它不会工作。

我认为您可以使用 Observer Forever,它不需要生命周期所有者接口,并且您可以从视图模型中观察结果

对架构组件使用 Kotlin 协程。

可以使用 liveData构建器函数调用 suspend函数,将结果作为 LiveData对象提供。

val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}

还可以从块中发出多个值。每个 emit()调用都会挂起块的执行,直到在主线程上设置了 LiveData值为止。

val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}

在分级配置中,使用 androidx.lifecycle:lifecycle-livedata-ktx:2.2.0或更高。

还有一个关于它的 文章

更新 : 也可以改变 Dao interface中的 LiveData<YourData>。您需要向函数添加 suspend关键字:

@Query("SELECT * FROM the_table")
suspend fun getAll(): List<YourData>

ViewModel中,你需要像这样异步得到它:

viewModelScope.launch(Dispatchers.IO) {
allData = dao.getAll()
// It's also possible to sync other data here
}

使用 Flow

文档中的指导方针被误解了

然而,ViewModel 对象绝不能观察对生命周期感知的可观察对象(例如 LiveData 对象)的更改。

在这个 Github 的问题中,他描述了应用上述规则的情况,即观察到的生命周期感知的可观察对象由另一个生命周期范围托管。 在 ViewModel中观察到的 LiveData包含观察到的 LiveData是没有问题的。

使用 Flow

class MyViewModel : ViewModel() {
private val myLiveData = MutableLiveData(1)


init {
viewModelScope.launch {
myLiveData.asFlow().collect {
// Do Something
}
}
}
}

使用 StateFlow

class MyViewModel : ViewModel() {
private val myFlow = MutableStateFlow(1)
private val myLiveData = myFlow.asLiveData(viewModelScope.coroutineContext)
}

附言

asFlow产生一个流,使 LiveData在启动 collect时被激活。我认为使用 MediatorLiveDataTransformations并附加虚拟观察器的解决方案在使用 Flow时没有区别,除了在 ViewModel实例中总是观察到从 LiveData发出的值之外。

I know there have already been amazing answers for this topic, but I wanted to add my own as well:

如果你想坚持使用 LiveData,你可以一直使用 Transformations.map,这样你就不必在 ViewModel中使用 observe,而只需在 Fragment/Activity中使用 observe

否则,您可以使用 SharedFlow,一个单一的事件可观察

您不必在 ViewModel中传递 viewLifecycleOwner,因为当 View只需要最新的结果时,在 ViewModel中调用 observe是没有意义的。

这距离最初的帖子已经有一段时间了,但是我最近偶然发现了同样的问题(也是 Firebase 的问题) ,并且我能够用 Transformations 解决它。

我有一个存储库类,其中包含由 Firebase 的 ValueEventListener 收集的 liveData 对象。 ViewModel 保存对此存储库的引用。

现在,在 ViewModel 中,没有一个函数从存储库返回 LiveData 值,然后通过观察者将其传递给片段,如下所示:

fun getMyPayments(): LiveData<HashMap<String, Int>> {
return repository.provideMyPayments()
}

我在 Transformations.map 中使用了一个 val,它表示由 ViewModel 中的另一个函数处理后 LiveData 的最终结果:

val myRoomPaymentsList : LiveData<HashMap<String, HashMap<String, Payment>>> = Transformations.map(repository.provideMyPayments()) {data ->
getRoomPaymentsList(data)
}

请注意,第一个参数是您观察到的数据源,第二个参数是您想要得到的结果。 这个 val 是一个 LiveData val,它保存存储库中最新的值,并根据需要在片段中提供服务,将所有处理都保存在 ViewModel 中,只将 UI 函数保存在 Framgent 本身中。

然后,在我的片段中,我把一个观察者放在这个 val 上:

viewModel.myRoomPaymentsList.observe(viewLifecycleOwner, {
roomPayments = it
graphFilterPeriod()
})

例如,如果您需要获取一个 ID (作为 LiveData)并使用它为 LiveData 进行另一个调用。将 ID 存储在 selectedID 中,转换将观察该字段,并在该字段发生变化时调用 getAllByID (selectedID)(也是 LiveData)。

var selectedID = MutableLiveData<Int>()


val objects = Transformations.switchMap(selectedID) { getAllByID(it) }