Home > Blockchain >  Why LiveData is not updating from coroutine?
Why LiveData is not updating from coroutine?

Time:12-06

I have function, that reaches API and puts recieved object into database. Then this function returns Long as id for inserted object. I want to run this function in the backthread with coroutine and still get the id of newly inserted object.

But after I run function in fragment LiveData stays null and gets correct id only on second button click.

Is this because i can't reach anything from coroutin or simply request and db insert take time and I have to wait for "urlEntityId" to update?

Here is code from 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
        }

    }

}

And my ViewModel code:

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)

}}

CodePudding user response:

Launching a coroutine is asynchronous. The code in the launch block is queued to be run as a coroutine, but the code after the launch block will likely be reached first.

You are getting the value of your LiveData before your coroutine has had a chance to run.

You should rarely ever use the value property of a LiveData in your Fragment. The point of the LiveData is that you can observe it and react after it has changed. If you were going to just wait for the data to be ready and use it, your Main thread would be locked up waiting for the data to be fetched and freeze the UI.

So, in your ViewModel function, you need to move your isLoading false call inside the coroutine so it doesn't stop showing loading state until after the data is ready:

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

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

}}

And in your Fragment, you should observe the LiveData instead of immediately trying to use its 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)
    }
}

I also think you need to set up your navigation so this fragment is removed from the back stack when it goes to the result fragment. Otherwise, when you back out of the result fragment, this one will immediately reopen the search result. Alternatively, you could add a function to your ViewModel that clears the most recent search results by setting the LiveData back to null (you will have to make its type nullable). Call this clear function in the Fragment's observer.

  • Related