#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 в наблюдателе фрагмента.