为啥 LiveData 没有从协程更新?

Posted

技术标签:

【中文标题】为啥 LiveData 没有从协程更新?【英文标题】:Why LiveData is not updating from coroutine?为什么 LiveData 没有从协程更新? 【发布时间】:2022-01-11 16:18:01 【问题描述】:

我有功能,它到达 API 并将接收到的对象放入数据库。然后此函数返回 Long 作为插入对象的 id。我想用协程在后台运行这个函数,仍然得到新插入对象的id。

但是在我在片段中运行函数后 LiveData 保持为空,并且仅在第二个按钮单击时获得正确的 id。

这是因为我无法从 cooutin 获得任何东西,或者只是请求和数据库插入需要时间,我必须等待“urlEntityId”更新?

这是来自 Fragment 的代码:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) 
    super.onViewCreated(view, savedInstanceState)

    binding.etSearch.apply 

        setOnEditorActionListener  v, actionId, event ->
            if (actionId == EditorInfo.IME_ACTION_SEARCH) 

                val pattern = DOMAIN_VALIDATION
                val url = this.text.toString()

                if (url.matches(pattern)) 

                    viewModel.loadUrlModelFromApi(url)
                    val action =
                        SearchFragmentDirections.actionSearchFragmentToResultFragment(
                            viewModel.urlEntityId.value ?: 1L
                        )

                    navController?.navigate(action)

                 else 
                    binding.textLayout.error = "Please enter correct domain name."
                
                true
             else 
                false
            

        

        doOnTextChanged  text, start, before, count ->
            binding.textLayout.error = null
        

    


还有我的 ViewModel 代码:

class SearchViewModel @Inject constructor(private val repository: UrlRepository) : ViewModel() 

private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading

private val _toastError = MutableLiveData<String>()
val toastError: LiveData<String> = _toastError

private val _urlEntity = MutableLiveData<UrlEntity>()
val urlEntity: LiveData<UrlEntity> = _urlEntity

private var _urlEntityId = MutableLiveData<Long>()
var urlEntityId: LiveData<Long> = _urlEntityId

fun loadUrlModelFromApi(domainName: String) 
    _isLoading.postValue(true)

    viewModelScope.launch(Dispatchers.IO) 
        _urlEntityId.postValue(repository.getUrl(domainName))
    
    _isLoading.postValue(false)


【问题讨论】:

_isLoading.postValue(false) 放入launch 块中。 launch 立即返回,这就是为什么 _isLoading 值在 repository.getUrl 函数被调用之前更改为 false 的原因。此外,您不能在调用loadUrlModelFromApi 后立即使用_urlEntityId。它还没有收到价值。您需要观察实时数据,然后进行导航。 是的,它成功了,谢谢! 【参考方案1】:

启动协程是异步的。 launch 块中的代码排队等待作为协程运行,但 launch 块之后的代码可能会首先到达。

在您的协程有机会运行之前,您已经获得了 LiveData 的值。

您应该很少在 Fragment 中使用 LiveData 的 value 属性。 LiveData 的重点是您可以观察它并在它发生变化后做出反应。如果您只是等待数据准备好并使用它,您的主线程将被锁定等待获取数据并冻结 UI。

因此,在您的 ViewModel 函数中,您需要将您的 isLoading false 调用移动到协程内,以便在数据准备好之前它不会停止显示加载状态:

fun loadUrlModelFromApi(domainName: String) 
    _isLoading.postValue(true)

    viewModelScope.launch(Dispatchers.IO) 
        _urlEntityId.postValue(repository.getUrl(domainName))
        _isLoading.postValue(false)
    


在你的 Fragment 中,你应该观察 LiveData 而不是立即尝试使用它的value

override fun onViewCreated(view: View, savedInstanceState: Bundle?) 
    super.onViewCreated(view, savedInstanceState)

    binding.etSearch.apply 

        setOnEditorActionListener  v, actionId, event ->
            if (actionId == EditorInfo.IME_ACTION_SEARCH) 

                val pattern = DOMAIN_VALIDATION
                val url = this.text.toString()

                if (url.matches(pattern)) 
                    viewModel.loadUrlModelFromApi(url)
                 else 
                    binding.textLayout.error = "Please enter correct domain name."
                
                true
             else 
                false
            

        

        doOnTextChanged  text, start, before, count ->
            binding.textLayout.error = null
        

    

    viewModel.urlEntityId.observe(this) 
        val action = SearchFragmentDirections.actionSearchFragmentToResultFragment(
            viewModel.urlEntityId.value ?: 1L
        )
        navController?.navigate(action)
    

我还认为您需要设置导航,以便该片段在进入结果片段时从后台堆栈中删除。否则,当您退出结果片段时,此片段将立即重新打开搜索结果。或者,您可以向 ViewModel 添加一个函数,通过将 LiveData 设置回 null 来清除最近的搜索结果(您必须使其类型为 null)。在 Fragment 的观察者中调用这个 clear 函数。

【讨论】:

以上是关于为啥 LiveData 没有从协程更新?的主要内容,如果未能解决你的问题,请参考以下文章

一个Bug事件,引发我对 LiveData 与 协程 的思考

Kotlin 用Retrofit+OkHttp+协程+LiveData搭建MVVM(Jetpack)来实现网络请求(网络数据JSON解析)显示在RecyclerView(更新中)

纯kotlin+ViewModel+LiveData+协程MVVM

再谈协程之viewmodel-livedata难兄难弟

2021-08-13 解决协程调用LiveData异常后LiveData监听失效BUG

Android - ViewModel、LiveData、Room 和 Retrofit 以及协程放在 kotlin 中