3 votes

Coroutines, DiffUtil asynchrone et erreur d'incohérence détectée

J'ai du mal à mettre ensemble Kotlin Flows et async DiffUtil.

J'ai cette fonction dans mon RecyclerView.Adapter qui calcule sur un thread de calcul un DiffUtil et envoie les mises à jour au RecyclerView sur le thread principal :

suspend fun updateDataset(newDataset: List<Item>) = withContext(Dispatchers.Default) {
        val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback()
        {
            override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
                    = dataset[oldItemPosition].conversation.id == newDataset[newItemPosition].conversation.id

            override fun getOldListSize(): Int = dataset.size
            override fun getNewListSize(): Int = newDataset.size

            override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean
                    = dataset[oldItemPosition] == newDataset[newItemPosition]
        })

        withContext(Dispatchers.Main) {
            dataset = newDataset // <-- dataset is the Adapter's dataset
            diff.dispatchUpdatesTo(this@ConversationsAdapter)
        }
    }

J'appelle cette fonction depuis mon Fragment comme ceci :

private fun updateConversationsList(conversations: List<ConversationsAdapter.Item>)
{
    viewLifecycleOwner.lifecycleScope.launch {
        (listConversations.adapter as ConversationsAdapter).updateDataset(conversations)
    }
}

updateConversationsList() est appelée plusieurs fois dans un laps de temps très court, car cette fonction est appelée par la fonction de Kotlin Flows comme Flow<Conversation> .

Maintenant avec tout ça, j'ai parfois une java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder erreur. Lecture du site ce fil Je comprends qu'il s'agit d'un problème d'enfilage et j'ai lu beaucoup de recommandations du genre celui-ci qui disent tous : le thread qui met à jour le jeu de données de l'adaptateur et le thread qui distribue les mises à jour au RecyclerView doivent être les mêmes.

Comme vous pouvez le constater, je respecte déjà ce principe en faisant :

withContext(Dispatchers.Main) {
    dataset = newDataset
    diff.dispatchUpdatesTo(this@ConversationsAdapter)
}

Puisque le fil principal, et lui seul, effectue ces deux opérations, comment est-il possible que j'obtienne cette erreur ?

5voto

Pawel Points 5016

Votre différentiel est en train de s'emballer. Si votre mise à jour arrive deux fois dans une courte période, cela peut arriver :

Adapter has dataset 1 @Main
Dataset 2 comes
calculateDiff between 1 & 2 @Async
Dataset 3 comes
calculateDiff between 1 & 3 @Async
finished calculating diff between 1 & 2 @ Async
finished calculating diff between 1 & 3 @ Async
Dispatcher main starts handling messages
replace dataset 1 with dataset 2 using 1-2 diff @Main
replace dataset 2 with dataset 3 using 1-3 diff @Main - inconsistency

Le scénario alternatif est que la différence entre 1-3 peut se terminer avant 1-2 mais le problème reste le même. Vous devez annuler le calcul en cours lorsqu'un nouveau calcul est effectué et empêcher le déploiement d'une différence invalide, par exemple en stockant la référence du travail dans votre fragment :

var updateJob : Job? = null

private fun updateConversationsList(conversations: List<ConversationsAdapter.Item>)
{
    updateJob?.cancel()
    updateJob = viewLifecycleOwner.lifecycleScope.launch {
        (listConversations.adapter as ConversationsAdapter).updateDataset(conversations)
    }
}

Si vous l'annulez, alors withContext(Dispatchers.Main) vérifiera en interne l'état de la continuation et ne s'exécutera pas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X