让每个观察者仅在订阅/观察时接收*新的LiveData

mue*_*flo 7 android android-lifecycle android-mvvm android-livedata android-architecture-components

每当您调用.observe()LiveData 时,观察者都会收到该 LiveData 的最后一个值。这在某些情况下可能有用,但对我来说没有用。

  1. 每当我调用 时.observe(),我希望观察者只接收未来的 LiveData 更改,而不是.observe()调用时它所持有的值。

  2. 对于一个 LiveData 实例,我可能有多个观察者。我希望他们都能在发生 LiveData 更新时收到更新。

  3. 我希望每个 LiveData 更新仅被每个观察者使用一次。我认为这只是对第一个要求的重新表述,但我的头已经开始旋转,我对此不确定。


在谷歌搜索这个问题时,我发现了两种常见的方法:

  1. 将数据包装在 an 中LiveData<SingleEvent<Data>>并检查该类SingleEvent是否已被消耗。

  2. 如果观察者已经获得事件,则扩展MediatorLiveData并使用查找映射

这些方法的示例可以在这里找到: https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#gistcomment-2783677 https://gist.github.com/hadilq/f095120348a6a14251a02aca329f1845#file-liveevent-kt https://gist .github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af#file-event-kt

不幸的是,这些例子都不能满足我的所有要求。大多数时候,问题是任何新的观察者在订阅时仍然收到最后的 LiveData 值。这意味着每当用户在屏幕之间导航时,已经显示的 Snackbar 就会一次又一次地显示。


为了让您了解我正在谈论的内容/我正在编码的内容:

我遵循Android架构组件的LiveData MVVM设计:

  • 2 ListFragment 显示条目列表。
  • 他们使用同一 ViewModel 类的 2 个实例来观察与 UI 相关的 LiveData。
  • 用户可以删除此类 ListFragment 中的条目。删除是通过 ViewModel 调用完成的Repository.delete()
  • ViewModel 观察 的存储库RepositoryEvents

因此,当删除完成后,Repository 会通知 ViewModel,ViewModel 也会通知 ListFragment。

现在,当用户切换到第二个 ListFragment 时,会发生以下情况:

  • 创建第二个 Fragment 并调用.observe()其 ViewModel
  • ViewModel 被创建并调用.observe()存储库

  • 存储库将其当前数据发送RepositoryEvent到 ViewModel

  • ViewModel 将相应的 UI 事件发送到 Fragment
  • 该片段显示了一个确认 Snackbar,用于确认其他地方发生的删除。

这是一些简化的代码:

分段:

viewModel.dataEvents.observe(viewLifecycleOwner, Observer { showSnackbar() })
viewModel.deleteEntry()
Run Code Online (Sandbox Code Playgroud)

视图模型:

val dataEvents: LiveData<EntryListEvent> = Transformations.switchMap(repository.events, ::handleRepoEvent)
fun deleteEntry() = repository.deleteEntry()
private fun handleRepoEvent(event: RepositoryEvent): LiveData<EntryListEvent> {
    // convert the repository event to an UI event
}
Run Code Online (Sandbox Code Playgroud)

存储库:

private val _events = MutableLiveData<RepositoryEvent>()
val events: LiveData<RepositoryEvent>
    get() = _events

fun deleteEntry() {
    // delete it from database
    _events.postValue(RepositoryEvent.OnDeleteSuccess)
}
Run Code Online (Sandbox Code Playgroud)

mue*_*flo 3

2021 年更新:

使用协程库和 Flow 现在可以很容易地通过实现来实现这一点Channels

主要活动

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import com.plcoding.kotlinchannels.databinding.ActivityMainBinding
import kotlinx.coroutines.flow.collect

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MainViewModel

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        binding.btnShowSnackbar.setOnClickListener {
            viewModel.triggerEvent()
        }

        lifecycleScope.launchWhenStarted {
            viewModel.eventFlow.collect { event ->
                when(event) {
                    is MainViewModel.MyEvent.ErrorEvent -> {
                        Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG).show()
                    }
                }
            }
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

主视图模型

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

class MainViewModel : ViewModel() {

    sealed class MyEvent {
        data class ErrorEvent(val message: String): MyEvent()
    }

    private val eventChannel = Channel<MyEvent>()
    val eventFlow = eventChannel.receiveAsFlow()

    fun triggerEvent() = viewModelScope.launch {
        eventChannel.send(MyEvent.ErrorEvent("This is an error"))
    }
}
Run Code Online (Sandbox Code Playgroud)