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.