Android: RecyclerView notifyItemChanged не работает при перезаходе во фрагмент
В Android приложении есть фрагмент с RecyclerView на котором отображаются элементы.
Изначально у элементов нет данных, но при нажатии на них, для каждого элемента запускается отдельная функция loadElementData, которая подгружает данные в элемент.
У каждого элемента на момент загрузки отображается крутящийся ProgressBar.
Проблем с загрузкой данных нет, все работает.
Проблема именно в отображении индикации загрузки, а точнее в том, что в одном из сценариев индикатор не пропадает.
Поскольку viewModel фрагмента работает на уровне Activity, чтобы пользователь мог выйти из фрагмента и загрузка не отменилась, есть 4 возможных сценария:
Пользователь нажал на элемент, индикатор отобразился, данные загрузились, индикатор скрылся. — Работает
Пользователь нажал на элемент, индикатор отобразился, пользователь перезашел в этот же фрагмент, но с другим набором элементов, загрузка отменилась, после входа во фрагмент с загружающимися элементами они уже не грузятся, индикатор скрыт. — Работает
Пользователь нажал на элемент, индикатор отобразился, пользователь вышел из фрагмента, данные загрузились, пользователь вернулся во фрагмент, данные загружены, индикатор скрыт. — Работает
Пользователь нажал на элемент, индикатор отобразился, пользователь вышел из фрагмента и сразу зашел обратно, данные загружаются, индикатор отображен, данные загрузились, индикатор все еще отображается.
Проблема именно с 4 сценарием. Почему-то именно в таком случае индикатор загрузки не пропадает. С чем это может быть связано и как решить проблему?
Код:
private fun loadItemData(item: MyItem) {
val index = viewModel.currentList.value!!.indexOf(item)
item.isDataLoading = true
itemAdapter.notifyItemChanged(index)
val job = requireActivity().lifecycleScope.launchPausing(Dispatchers.IO) {
var itemData: ItemData? = null
try {
itemData = async { getItemData(item.id) }.await()
itemData?.let { item.data = it }
} catch (e: CancellationException){
Log.d("debug", "loading canceled")
} finally {
item.isDataLoading = false
activity?.runOnUiThread {
itemAdapter.notifyItemChanged(index)
}
}
}
viewModel.loadItemDataJobs.add(job)
requireActivity().lifecycleScope.launch(Dispatchers.IO) {
job.join()
if (job in viewModel.loadItemDataJobs)
viewModel.loadItemDataJobs.remove(job)
}
}
С помощью библиотеки pausing-coroutine-dispatcher я могу приостанавливать корутины и возобновлять их в onPause и onResume. Такие корутины запускаются с помощью launchPausing
implementation 'com.github.Koitharu:pausing-coroutine-dispatcher:1.0'
Ответы (1 шт):
Поскольку в рамках данной задачи подразумевается выход из фрагмента, эту задачу точно нужно выполнять не в самом фрагменте, а во ViewModel.
Таким образом, я решил, что будет правильнее при нажатии на элемент отображать на нем индикатор загрузки, вызывать notifyItemChanged, а затем запускать загрузку в методе из ViewModel, а данные получать уже через LiveData, когда точно известно, что фрагмент и RecyclerView есть.
Логика примерно такая:
Fragment.kt
loadItemData вызываем при нажатии на элемент
observeItemDataResponses вызываем при создании View, чтобы при изменениях адаптер автоматически уведомлялся о загруженных элементах
private fun loadItemData(item: MyItem){
val index = viewModel.currentList.value!!.indexOf(item)
item.isDataLoading = true
itemAdapter.notifyItemChanged(index)
viewModel.loadItemData(currentListTag, item)
}
private fun observeItemDataResponses(){
viewModel.itemDataResponses.observe(viewLifecycleOwner) { responses ->
responses.filter { it.first.isDataLoading }.forEach { response ->
val item = response.first
val itemDataResponse = response.second
itemDataResponse.data?.let { data ->
item.data = data
}
item.isDataLoading = false
viewModel.currentItemList.value?.let { itemList ->
if (item in itemList){
itemAdapter.notifyItemChanged(itemList.indexOf(item))
if (itemDataResponse.status == Status.ERROR)
viewModel.removeItemFromList(item)
}
}
}
}
}
Здесь currentListTag — это какой-то общий признак, по которому элементы объединены в этот список. Необходим, т.к. пользователь может перезайти во фрагмент уже с другим тегом. для которого будет отображаться другой список элементов.
ViewModel.kt
private val _itemDataResponses = MutableLiveData<List<Pair<MyItem, ItemData>>>()
val itemDataResponses: LiveData<List<Pair<MyItem, ItemData>>> get() = _itemDataResponses
private fun addItemDataResponse(item: MyItem, response: ItemData){
val currentResponses = _itemDataResponses.value.orEmpty().toMutableList()
currentResponses.add(Pair(item, response))
_itemDataResponses.postValue(currentResponses)
}
// currentListTag: Job
private val loadItemDataJobs = HashMap<String, MutableList<Deferred<ItemData>>>()
private fun stopLoadItemDataJobs(currentListTag: String){
loadItemDataJobs.getOrDefault(currentListTag, null)?.let { loadingJobs ->
loadingJobs.forEach { job ->
job.cancel()
}
loadItemDataJobs.remove(currentListTag)
}
}
fun loadItemData(currentListTag: String, item: MyItem) {
val loadingJobs = loadItemDataJobs.getOrPut(currentListTag) { mutableListOf() }
val job = viewModelScope.async(Dispatchers.IO) {
getItemData(item.id)
}
loadingJobs.add(job)
viewModelScope.launch(Dispatchers.IO) {
val itemDataResponse = try{
job.await()
} catch (e: CancellationException){
ItemData(status = Status.CANCELED)
}
addItemDataResponse(item, itemDataResponse)
if (job in loadingJobs)
loadingJobs.remove(job)
}
}
По тегу currentListTag в дальнейшем можно отменить все начатые задачи.
При выходе из фрагмента имеет смысл очищать _itemDataResponses от уже загруженных элементов.