为什么 LiveData 不从协程更新?

Why LiveData is not updating from coroutine?

我有功能,达到 API 并将收到的对象放入数据库。然后这个函数 returns Long 作为插入对象的 id。我想 运行 在带有协程的后台线程中使用此函数,并且仍然获得新插入对象的 ID。

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

这是因为我无法从 coroutin 获取任何信息,还是只是请求和数据库插入需要时间,我必须等待“urlEntityId”更新?

这是片段中的代码:

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)

}}

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

在协程有机会 运行 之前,您正在获取 LiveData 的值。

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

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

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 的观察者中调用这个清除函数。