[英]MVVM: Complex View/ViewModel -> Multiple LiveData objects?
[英]Communication between view and ViewModel in MVVM with LiveData
在ViewModel
和View
之间进行通信的正确方法是什么, Google architecture components
使用LiveData
,其中视图订阅更改并相应地更新自身,但是这种通信不适用于单个事件,例如显示消息、显示进度、隐藏进度等
在 Google 的示例中有一些像SingleLiveEvent
这样的SingleLiveEvent
,但它仅适用于 1 个观察者。 一些开发人员使用EventBus
但我认为当项目增长时它会很快失控。
有没有方便正确的实现方式,你是如何实现的?
(也欢迎 Java 示例)
是的,我同意, SingleLiveEvent
是一个笨拙的解决方案,而 EventBus(以我的经验)总是会导致麻烦。
不久前,我在阅读 Google CodeLabs for Kotlin Coroutines 时发现了一个名为ConsumableValue
的类,我发现它是一个很好的、干净的解决方案,对我很有帮助( ConsumableValue.kt ):
class ConsumableValue<T>(private val data: T) {
private var consumed = false
/**
* Process this event, will only be called once
*/
@UiThread
fun handle(block: ConsumableValue<T>.(T) -> Unit) {
val wasConsumed = consumed
consumed = true
if (!wasConsumed) {
this.block(data)
}
}
/**
* Inside a handle lambda, you may call this if you discover that you cannot handle
* the event right now. It will mark the event as available to be handled by another handler.
*/
@UiThread
fun ConsumableValue<T>.markUnhandled() {
consumed = false
}
}
class MyViewModel : ViewModel {
private val _oneShotEvent = MutableLiveData<ConsumableValue<String>>()
val oneShotEvent: LiveData<ConsumableValue<String>>() = _oneShotData
fun fireEvent(msg: String) {
_oneShotEvent.value = ConsumableValue(msg)
}
}
// In Fragment or Activity
viewModel.oneShotEvent.observe(this, Observer { value ->
value?.handle { Log("TAG", "Message:$it")}
})
简而言之, handle {...}
块只会被调用一次,因此如果您返回到屏幕,则无需清除该值。
使用 Kotlin Flow怎么样?
我不相信它们具有与 LiveData 相同的行为,它总是为您提供最新的价值。 它只是一个类似于 LiveData 的解决方法SingleLiveEvent
的订阅。
这是一个解释差异的视频,我认为您会发现有趣并回答您的问题
您可以通过不使用 LiveData 来轻松实现这一点,而是使用我专门为解决此问题而编写的Event-Emitter库,而不依赖 LiveData(这是 Google 概述的反模式,我不知道任何其他相关替代方案) 。
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
implementation 'com.github.Zhuinden:event-emitter:1.0.0'
如果您还复制LiveEvent
类,那么现在您可以执行
private val emitter: EventEmitter<String> = EventEmitter()
val events: EventSource<String> get() = emitter
fun doSomething() {
emitter.emit("hello")
}
和
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = getViewModel<MyViewModel>()
viewModel.events.observe(viewLifecycleOwner) { event ->
// ...
}
}
// inline fun <reified T: ViewModel> Fragment.getViewModel(): T = ViewModelProviders.of(this).get(T::class.java)
对于基本原理,您可以查看我写的解释为什么替代方案不是有效方法的文章。
但是,您现在也可以使用Channel(UNLIMITED)
并使用asFlow()
将其公开为流。 这在 2019 年并不真正适用。
尝试这个:
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
并将其包装到 LiveData 中
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
并观察
myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
链接参考: 使用事件包装器
为了在屏幕加载时显示/隐藏进度对话框和显示来自失败的网络调用的错误消息,您可以使用封装了 View 正在观察的 LiveData 的包装器。
有关此方法的详细信息在应用程序架构的附录中: https : //developer.android.com/jetpack/docs/guide#addendum
定义一个资源:
data class Resource<out T> constructor(
val state: ResourceState,
val data: T? = null,
val message: String? = null
)
和一个资源状态:
sealed class ResourceState {
object LOADING : ResourceState()
object SUCCESS : ResourceState()
object ERROR : ResourceState()
}
在 ViewModel 中,使用包含在 Resource 中的模型定义 LiveData:
val exampleLiveData = MutableLiveData<Resource<ExampleModel>>()
同样在 ViewModel 中,定义调用 API 以加载当前屏幕数据的方法:
fun loadDataForView() = compositeDisposable.add(
exampleUseCase.exampleApiCall()
.doOnSubscribe {
exampleLiveData.setLoading()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
exampleLiveData.setSuccess(it)
},
{
exampleLiveData.setError(it.message)
}
)
)
在视图中,在创建时设置观察者:
viewModel.exampleLiveData.observe(this, Observer {
updateResponse(it)
})
这是示例updateResponse()
方法,显示/隐藏进度,并在适当时显示错误:
private fun updateResponse(resource: Resource<ExampleModel>?) {
resource?.let {
when (it.state) {
ResourceState.LOADING -> {
showProgress()
}
ResourceState.SUCCESS -> {
hideProgress()
// Use data to populate data on screen
// it.data will have the data of type ExampleModel
}
ResourceState.ERROR -> {
hideProgress()
// Show error message
// it.message will have the error message
}
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.