Android: RecyclerView notifyItemChanged не работает при перезаходе во фрагмент

В Android приложении есть фрагмент с RecyclerView на котором отображаются элементы.

Изначально у элементов нет данных, но при нажатии на них, для каждого элемента запускается отдельная функция loadElementData, которая подгружает данные в элемент.

У каждого элемента на момент загрузки отображается крутящийся ProgressBar.

Проблем с загрузкой данных нет, все работает.

Проблема именно в отображении индикации загрузки, а точнее в том, что в одном из сценариев индикатор не пропадает.


Поскольку viewModel фрагмента работает на уровне Activity, чтобы пользователь мог выйти из фрагмента и загрузка не отменилась, есть 4 возможных сценария:

  1. Пользователь нажал на элемент, индикатор отобразился, данные загрузились, индикатор скрылся. — Работает

  2. Пользователь нажал на элемент, индикатор отобразился, пользователь перезашел в этот же фрагмент, но с другим набором элементов, загрузка отменилась, после входа во фрагмент с загружающимися элементами они уже не грузятся, индикатор скрыт. — Работает

  3. Пользователь нажал на элемент, индикатор отобразился, пользователь вышел из фрагмента, данные загрузились, пользователь вернулся во фрагмент, данные загружены, индикатор скрыт. — Работает

  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 шт):

Автор решения: denisnumb

Поскольку в рамках данной задачи подразумевается выход из фрагмента, эту задачу точно нужно выполнять не в самом фрагменте, а во 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 от уже загруженных элементов.

→ Ссылка