繁体   English   中英

MVVM 中视图和 ViewModel 之间的通信与 LiveData

[英]Communication between view and ViewModel in MVVM with LiveData

ViewModelView之间进行通信的正确方法是什么, 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的订阅。

这是一个解释差异的视频,我认为您会发现有趣并回答您的问题

https://youtu.be/B8ppnjGPAGE?t=535

您可以通过不使用 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM