簡體   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