3 votes

L'utilisation de DiffUtil et d'un adaptateur personnalisé provoque une incohérence détectée

Actuellement, j'ai essayé d'utiliser DiffUtil pour notifier les éléments du RecyclerView dans mon projet.

Pendant ce temps, j'ai écrit un adaptateur personnalisé qui pouvait ajouter des en-têtes et des pieds de page. J'ai utilisé cet adaptateur et ajouté un pied de page load more pour que ma liste puisse se charger davantage lors du défilement vers le bas. Cela m'a conduit à un problème : ma liste qui pourrait être plus chargée contiendra toujours au moins un élément (load more footer).

Pour éviter tout malentendu, le mot "articles" ci-dessous signifie spécifiquement que les articles ne sont pas des en-têtes ou des pieds de page.

Puis le problème est apparu lorsque j'ai notifié les éléments par diffUtil de n (n > 0) éléments à zéro, mon application se plante.

Voici l'exception : java.lang.IndexOutOfBoundsException : Incohérence détectée. L'adaptateur de porte-vue positionViewHolder est invalide.

Il est intéressant de mentionner que si j'utilise l'adaptateur personnalisé sans en-tête ni pied de page tout ira bien.

J'ai cherché des solutions mais aucune de leurs situations n'est la même pour moi.

Voici les codes (Java) de l'adaptateur personnalisé, diffUtil est envoyé des mises à jour à l'adaptateur. innerAdapter en elle :

public class WrapperAdapter extends RecyclerView.Adapter {

    // 0x40000000  1 flag 1 HEADER 0  FOOTER
    private static final int HEADER_FOOTER_TYPE_MASK = 0x40000000;
    // 0x3f0000  6 index  HEADER FOOTER  index 64 HEADER 64 FOOTER
    private static final int HEADER_FOOTER_INDEX_MASK = 0x3f000000;
    //  24  view  hash code  HEADERFOOTER  viewType
    private static final int HEADER_FOOTER_HASH_MASK = 0x00ffffff;

    private RecyclerView.Adapter innerAdapter;
    private RecyclerView.AdapterDataObserver innerObserver = new AdapterDataObserverProxy();
    private List<View> headers = new ArrayList<>();
    private List<View> footers = new ArrayList<>();

    public WrapperAdapter(@NonNull RecyclerView.Adapter adapter) {
        innerAdapter = adapter;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (viewType < 0) {
            if ((viewType & HEADER_FOOTER_TYPE_MASK) != 0) {
                // HEADER
                int headerIndex = (viewType & HEADER_FOOTER_INDEX_MASK) >> 24;
                return new InnerViewHolder(headers.get(headerIndex));
            } else {
                // FOOTER
                int footerIndex = (viewType & HEADER_FOOTER_INDEX_MASK) >> 24;
                return new InnerViewHolder(footers.get(footerIndex));
            }
        }
        return innerAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (isHeader(position) || isFooter(position)) {
            return;
        }

        innerAdapter.onBindViewHolder(holder, position - headers.size());
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) {
        if (isHeader(position) || isFooter(position)) {
            return;
        }

        innerAdapter.onBindViewHolder(holder, position - headers.size(), payloads);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeader(position))  {
            return Integer.MIN_VALUE | HEADER_FOOTER_TYPE_MASK | ((position & (HEADER_FOOTER_INDEX_MASK >> 24)) << 24) | (headers.get(position).hashCode() & HEADER_FOOTER_HASH_MASK);
        }

        if (isFooter(position)) {
            int footerIndex = position - innerAdapter.getItemCount() - headers.size();
            return Integer.MIN_VALUE | ((footerIndex & (HEADER_FOOTER_INDEX_MASK >> 24)) << 24) | (footers.get(footerIndex).hashCode() & HEADER_FOOTER_HASH_MASK);
        }

        int innerViewType = innerAdapter.getItemViewType(position - headers.size());
        if (innerViewType < 0) {
            throw new IllegalArgumentException("View type cannot be negative, which is claimed by HEADER and FOOTER");
        }
        return innerViewType;
    }

    @Override
    public int getItemCount() {
        if (innerAdapter.getItemCount() == 0) {
            return headers.size();
        } else {
            return innerAdapter.getItemCount() + headers.size() + footers.size();
        }
    }

    private boolean isHeader(int position) {
        return position < headers.size();
    }

    private boolean isFooter(int position) {
        return position > getItemCount() - footers.size() - 1;
    }

    private class InnerViewHolder extends RecyclerView.ViewHolder {
        InnerViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }

    public void addHeader(@NonNull View header) {
        if (!headers.contains(header)) {
            headers.add(header);
            notifyItemInserted(headers.size() - 1);
        }
    }

    public void removeHeader(@NonNull View header) {
        if (headers.contains(header)) {
            int index = headers.indexOf(header);
            headers.remove(index);
            notifyItemRemoved(index);
        }
    }

    public void addFooter(@NonNull View footer) {
        if (!footers.contains(footer)) {
            footers.add(footer);
            notifyItemInserted(getItemCount() - 1);
        }
    }

    public void removeFooter(@NonNull View footer) {
        if (footers.contains(footer)) {
            int index = footers.indexOf(footer);
            footers.remove(index);
            notifyItemRemoved(headers.size() + innerAdapter.getItemCount() + index);
        }
    }

    @Override
    public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            super.onViewRecycled(holder);
        } else {
            innerAdapter.onViewRecycled(holder);
        }
    }

    @Override
    public boolean onFailedToRecycleView(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            return super.onFailedToRecycleView(holder);
        } else {
            return innerAdapter.onFailedToRecycleView(holder);
        }

    }

    @Override
    public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            super.onViewAttachedToWindow(holder);
        } else {
            innerAdapter.onViewAttachedToWindow(holder);
        }
    }

    @Override
    public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            super.onViewDetachedFromWindow(holder);
        } else {
            innerAdapter.onViewDetachedFromWindow(holder);
        }
    }

    @Override
    public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
        super.registerAdapterDataObserver(observer);
        innerAdapter.registerAdapterDataObserver(innerObserver);
    }

    @Override
    public void unregisterAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
        super.unregisterAdapterDataObserver(observer);
        innerAdapter.unregisterAdapterDataObserver(innerObserver);
    }

    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        innerAdapter.onAttachedToRecyclerView(recyclerView);
    }

    @Override
    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
        innerAdapter.onDetachedFromRecyclerView(recyclerView);
    }

    @Override
    public void setHasStableIds(boolean hasStableIds) {
        super.setHasStableIds(hasStableIds);
        innerAdapter.setHasStableIds(hasStableIds);
    }

    @Override
    public long getItemId(int position) {
        if (isHeader(position)) return super.getItemId(position);
        if (isFooter(position)) return super.getItemId(position);
        return innerAdapter.getItemId(position);
    }

    private class AdapterDataObserverProxy extends RecclerView.AdapterDataObserver {
        @Override
        public void onChanged() {
            WrapperAdapter.this.notifyDataSetChanged();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            WrapperAdapter.this.notifyItemRangeChanged(positionStart + WrapperAdapter.this.headers.size(), itemCount);
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
            WrapperAdapter.this.notifyItemRangeChanged(positionStart + WrapperAdapter.this.headers.size(), itemCount, payload);
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            WrapperAdapter.this.notifyItemRangeInserted(positionStart + WrapperAdapter.this.headers.size(), itemCount);
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            WrapperAdapter.this.notifyItemRangeRemoved(positionStart + WrapperAdapter.this.headers.size(), itemCount);
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            WrapperAdapter.this.notifyItemMoved(fromPosition + WrapperAdapter.this.headers.size(), toPosition + WrapperAdapter.this.headers.size());
        }
    }
}

Voici les principaux codes (Kotlin) du recyclerView qui utilise un adaptateur personnalisé :

    private var wrapperAdapter: WrapperAdapter? = null
    override fun setAdapter(adapter: Adapter<*>?) {
        wrapperAdapter = if (adapter != null) {
            WrapperAdapter(adapter)
        } else {
            null
        }
        super.setAdapter(wrapperAdapter)
    }

    fun addHeader(header: View) {
        wrapperAdapter?.addHeader(header)
    }

    fun addFooter(footer: View) {
        wrapperAdapter?.addFooter(footer)
    }

    fun removeHeader(header: View) {
        wrapperAdapter?.removeHeader(header)
    }

    fun removeFooter(footer: View) {
        wrapperAdapter?.removeFooter(footer)
    }

La réponse la plus utile de cette question a fonctionné pour moi. Mais je pense que cela évite juste le plantage au lieu de le résoudre pour s'assurer que cela ne causera plus d'exceptions. Je ne pense pas que ce soit une bonne solution, alors je viens chercher de l'aide.

1voto

bwt Points 10035

De la mise en œuvre de getItemCount() il semble que les pieds de page ne s'affichent que lorsqu'il y a des éléments. Lorsque le(s) dernier(s) élément(s) est (sont) supprimé(s), vous devez informer l'adaptateur wrapper que les pieds de page sont également supprimés.

Disons qu'il y a 2 en-têtes, 1 article, 3 pieds de page : getItemCount() les retours 6. Si l'élément interne est retiré, seul un retrait est propagé à partir de l'adaptateur interne, mais getItemCount() renvoie maintenant 2 au lieu du 5 attendu

Je suggère de modifier le AdapterDataObserverProxy :

@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
    WrapperAdapter.this.notifyItemRangeRemoved(positionStart + WrapperAdapter.this.headers.size(), itemCount);
    if (itemCount > 0 && innerAdapter.getItemCount() == 0 && footers.size() > 0) {
        // no more inner items, notify the removal of the footers
        int firstFooterPosition = headers.size();
        int footerCount = footers.size();
        WrapperAdapter.this.notifyItemRangeRemoved(firstFooterPosition, footerCount);
    }
}

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