Почему LiveData не обновляется из сопрограммы?

#android #kotlin #mvvm #kotlin-coroutines

#Android #котлин #mvvm #котлин-сопрограммы

Вопрос:

У меня есть функция, которая достигает API и помещает полученный объект в базу данных. Затем эта функция возвращает Long идентификатор вставленного объекта. Я хочу запустить эту функцию в обратном потоке с помощью сопрограммы и все равно получить идентификатор вновь вставленного объекта.

Но после запуска функции во фрагменте LiveData остается нулевым и получает правильный идентификатор только при втором нажатии кнопки.

Это потому, что я ничего не могу получить от сопрограммы или просто запрашиваю, и вставка бд занимает время, и мне приходится ждать обновления «urlEntityId»?

Вот код из фрагмента:

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {  super.onViewCreated(view, savedInstanceState)   binding.etSearch.apply {   setOnEditorActionListener { v, actionId, event -gt;  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 -gt;  binding.textLayout.error = null  }   }  }  

И мой код модели представления:

 class SearchViewModel @Inject constructor(private val repository: UrlRepository) : ViewModel() {  private val _isLoading = MutableLiveDatalt;Booleangt;() val isLoading: LiveDatalt;Booleangt; = _isLoading  private val _toastError = MutableLiveDatalt;Stringgt;() val toastError: LiveDatalt;Stringgt; = _toastError  private val _urlEntity = MutableLiveDatalt;UrlEntitygt;() val urlEntity: LiveDatalt;UrlEntitygt; = _urlEntity  private var _urlEntityId = MutableLiveDatalt;Longgt;() var urlEntityId: LiveDatalt;Longgt; = _urlEntityId  fun loadUrlModelFromApi(domainName: String) {  _isLoading.postValue(true)   viewModelScope.launch(Dispatchers.IO) {  _urlEntityId.postValue(repository.getUrl(domainName))  }  _isLoading.postValue(false)  }}  

Комментарии:

1. Положите _isLoading.postValue(false) внутрь launch блока. launch возвращает немедленно, поэтому _isLoading значение изменяется на false еще до repository.getUrl того, как функция будет вызвана. Также вы не можете использовать _urlEntityId его сразу после звонка loadUrlModelFromApi . Он еще не получил значения. Вам нужно наблюдать за этими живыми данными, а затем перемещаться.

2. Да, это сработало, спасибо!

Ответ №1:

Запуск сопрограммы является асинхронным. Код в launch блоке помещен в очередь для запуска в качестве сопрограммы, но код после launch блока, скорее всего, будет достигнут первым.

Вы получаете ценность ваших живых данных еще до того, как у вашей сопрограммы появится возможность запустить ее.

Вы редко должны использовать value свойство LiveData в своем фрагменте. Смысл живых данных в том, что вы можете наблюдать за ними и реагировать после того, как они изменились. Если бы вы собирались просто подождать, пока данные будут готовы, и использовать их, ваш основной поток был бы заблокирован в ожидании извлечения данных и замораживания пользовательского интерфейса.

Итак, в вашей функции ViewModel вам нужно переместить isLoading ложный вызов внутри сопрограммы, чтобы он не прекращал показывать состояние загрузки до тех пор, пока данные не будут готовы:

 fun loadUrlModelFromApi(domainName: String) {  _isLoading.postValue(true)   viewModelScope.launch(Dispatchers.IO) {  _urlEntityId.postValue(repository.getUrl(domainName))  _isLoading.postValue(false)  }  }}  

И в своем Фрагменте вы должны наблюдать за живыми данными, а не сразу пытаться использовать их value .

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {  super.onViewCreated(view, savedInstanceState)   binding.etSearch.apply {   setOnEditorActionListener { v, actionId, event -gt;  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 -gt;  binding.textLayout.error = null  }   }   viewModel.urlEntityId.observe(this) {  val action = SearchFragmentDirections.actionSearchFragmentToResultFragment(  viewModel.urlEntityId.value ?: 1L  )  navController?.navigate(action)  } }  

Я также думаю, что вам нужно настроить навигацию таким образом, чтобы этот фрагмент удалялся из заднего стека, когда он переходит к фрагменту результата. В противном случае, когда вы отступите от фрагмента результата, этот фрагмент немедленно откроет результат поиска. В качестве альтернативы вы можете добавить в свою модель представления функцию, которая очищает самые последние результаты поиска, установив для LiveData значение null (вам придется сделать его тип обнуляемым). Вызовите эту функцию clear в наблюдателе фрагмента.