web-dev-qa-db-fra.com

onBindViewHolder () n'est jamais appelé dans la vue à la position même si RecyclerView.findViewHolderForAdapterPosition () renvoie null à cette position.

J'ai une liste avec 13 éléments (bien que des éléments puissent être ajoutés ou supprimés), positions 0-12. Lorsque le fragment contenant le RecyclerView est affiché pour la première fois, seules les positions 0 à 7 sont visibles pour l'utilisateur (la position 7 n'étant visible que de moitié). Dans mon adaptateur, je Log chaque fois qu'un détenteur de vue est lié/lié (idk si la grammaire s'applique ici) et enregistre sa position.

Adaptateur 

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
    Log.d(TAG, "onBindViewHolder() position: " + position);
    ...
}

Sur ma Log, je vois que les positions 0 à 7 sont liées:

 Log from Adapter

J'ai une méthode selectAll() qui récupère chaque ViewHolder par la position de l'adaptateur. Si le holder renvoyé n'est PAS null, j'utilise le holder renvoyé pour mettre à jour la vue afin d'afficher sa sélection. Si le titulaire renvoyé IS null j'appelle selectOnBind(), une méthode qui signale l'affichage de la vue lors de la mise à jour de la position pour indiquer qu'elle est sélectionnée lorsqu'elle est liée plutôt qu'en temps réel, car elle n'est pas affichée:

public void selectAll() {
    for (int i = 0; i < numberOfItemsInList; i++) {
        MyAdapter.ViewHolder holder = (MyAdapter.ViewHolder)
                mRecyclerView.findViewHolderForAdapterPosition(i);

        Log.d(TAG, "holder at position " + i + " is " + holder);

        if (holder != null) {
            select(holder);
        } else {
            selectOnBind(i);
        }
    }
}

Dans cette méthode, Log la holder avec sa position:

 Log from selectAll()

Donc, jusqu'à présent, tout semble normal. Nous avons les positions 0 à 7, et selon la variable Log, ce sont les positions liées. Lorsque je clique sur selectAll() sans modifier les vues visibles (défilement), je vois que les positions 0 à 7 sont définies et que 8 à 12 correspondent à null. Jusqu'ici tout va bien.

Voici où ça devient intéressant. Si, après avoir appelé selectAll(), je descends plus bas dans la liste, les positions 8 et 9 ne montrent pas qu'elles sont sélectionnées. 

En vérifiant la variable Log, je constate que c'est parce qu'ils ne sont jamais liés, même s'ils ont été déclarés être null:

 Adapter Log after scroll

Encore plus déroutant est que cela ne se produit pas à chaque fois. Si je lance d'abord l'application et que je teste cela, cela peut fonctionner. Mais cela semble se produire sans faute après. Je suppose que cela a quelque chose à voir avec le recyclage des vues, mais ne serait-il pas nécessaire qu'elles soient liées?

EDIT (6-29-16)
Après une mise à jour AndroidStudio, je n'arrive pas à reproduire le bogue. Cela fonctionne comme prévu, en liant les vues nulles. Si ce problème devait refaire surface, je reviendrai sur ce post.

21
YoungCoconutCode

Cela se produit parce que:

  • Les vues ne sont pas ajoutées à la vue de recyclage (getChildAt ne fonctionnera pas et renverra null pour cette position) 
  • Ils sont également mis en cache (onBind ne sera pas appelé) 

Appeler recyclerView.setItemViewCacheSize(0) résoudra ce "problème".

Comme la valeur par défaut est 2 (private static final int DEFAULT_CACHE_SIZE = 2; dans RecyclerView.Recycler), vous obtiendrez toujours 2 vues qui n'appelleront pas onBind mais qui ne seront pas ajoutées au recycleur.

15
Pedro Oliveira

Dans votre cas, les vues pour les positions 8 et 9 ne sont pas recyclées, elles sont détachées de la fenêtre et seront attachées à nouveau. Et pour ces vues détachées, onBindViewHolder n'est pas appelé, seul onViewAttachedToWindow est appelé. Si vous annulez ces fonctions dans votre adaptateur, vous pouvez voir de quoi je parle. 

@Override
    public void onViewRecycled(ViewHolder vh){
        Log.wtf(TAG,"onViewRecycled "+vh);
    }

    @Override
    public void onViewDetachedFromWindow(ViewHolder viewHolder){
        Log.wtf(TAG,"onViewDetachedFromWindow "+viewHolder);
    }

Maintenant, afin de résoudre votre problème, vous devez garder une trace des vues qui étaient supposées être recyclées mais se détacher et ensuite effectuer votre processus de section sur 

@Override
    public void onViewAttachedToWindow(ViewHolder viewHolder){
        Log.wtf(TAG,"onViewAttachedToWindow "+viewHolder);
    }
6
Zartha

Les réponses de Pedro Oliveira et Zartha sont excellentes pour comprendre le problème, mais je ne vois aucune solution qui me satisfasse.

Je pense que vous avez 2 bonnes options en fonction de ce que vous faites:

Option 1

Si vous souhaitez que onBindViewHolder() soit appelé pour une vue hors écran, qu'il soit mis en cache/détaché ou non, vous pouvez alors:

RecyclerView.ViewHolder view_holder = recycler_view.findViewHolderForAdapterPosition( some_position );

if ( view_holder != null )
{
    //manipulate the attached view
}
else //view is either non-existant or detached waiting to be reattached
    notifyItemChanged( some_position );

L'idée est que si la vue est mise en cache/détachée, alors notifyItemChanged() informera l'adaptateur que la vue est invalide, ce qui entraînera l'appel de onBindViewHolder().

Option 2

Si vous souhaitez uniquement exécuter une modification partielle (et pas tout ce qui se trouve à l'intérieur de onBindViewHolder()), à l'intérieur de onBindViewHolder( ViewHolder view_holder, int position ), vous devez stocker position dans le view_holder et exécuter le changement souhaité dans onViewAttachedToWindow( ViewHolder view_holder )

Je recommande l’option 1 pour plus de simplicité, à moins que votre onBindViewHolder() ne soit en train de faire quelque chose de très intensif, comme jouer avec Bitmaps.

1
Kacy

Je pense donc que @Pedro Oliveira répond à votre question ci-dessous. Le sens principal de RecycleView est qu’il utilise des algorithmes spéciaux pour mettre ViewHolder en cache à tout moment. Donc, onBindViewHolder (...) pourrait ne pas fonctionner, par exemple. si la vue est statique ou autre chose.

Et à propos de votre question, vous pensez utiliser RecycleView pour les vues modifiées dynamiquement. NE LE FAITES PAS! Parce que RecycleView invalide les vues et dispose d’un système de mise en cache, vous aurez donc beaucoup de problèmes. 

Utilisez LinkedListView pour cette tâche! 

0
GensaGames

Je pense que jouer avec la vue n’est pas une bonne idée dans Recyclerview. L'approche que j'utilise toujours pour suivre consiste simplement à introduire un indicateur dans le modèle à l'aide de RecyclerView. Supposons que votre modèle ressemble à - 

class MyModel{
    String name;
    int age;
}

Si vous suivez la vue, la vue est sélectionnée ou non, puis introduisez un booléen dans le modèle. Maintenant, ça va ressembler - 

class MyModel{
    String name;
    int age;
    boolean isSelected;
}

Maintenant, votre case à cocher sera sélectionnée/désélectionnée sur la base du nouvel indicateur isSelected (in onBindViewHolder ()). Lors de chaque sélection dans l'affichage, la valeur de la valeur sélectionnée du modèle correspondant devient true et, si elle n'est pas sélectionnée, elle est définie sur false. Dans votre cas, exécutez simplement une boucle pour remplacer la valeur isSelected de tous les modèles par true, puis appelez notifyDataSetChanged().

Par exemple, supposons que votre liste est 

ArrayList<MyModel> recyclerList;
private void selectAll(){
    for(MyModel myModel:recyclerList)
        myModel.isSelected = true;
    notifyDataSetChanged();
}

Ma suggestion, tout en utilisant recyclerView ou ListView pour moins essayer de jouer avec les vues. 

Donc dans votre cas - 

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
   holder.clickableView.setTag(position);
   holder.selectableView.setTag(position);
   holder.checkedView.setChecked(recyclerList.get(position).isSelected);
    Log.d(TAG, "onBindViewHolder() position: " + position);
    ...
}

@Override
public void onClick(View view){
    int position = (int)view.getTag();
    recyclerList.get(position).isSelected = !recyclerList.get(position).isSelected;
}

@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
      int position = (int)buttonView.getTag();
      recyclerList.get(position).isSelected = isChecked;
}

J'espère que cela vous aidera. S'il vous plaît, laissez-moi savoir si vous avez besoin d'explications supplémentaires :)

0
Neo

Si vous avez passé un grand nombre d'éléments dans la liste que vous avez passés à recyclerview adapter, vous ne rencontrerez pas le problème suivant: onBindViewHolder() n'est pas en cours d'exécution lors du défilement.

Mais si la liste contient moins d'éléments (j'ai vérifié la taille de la liste 5), vous pouvez rencontrer ce problème.

Une meilleure solution consiste à vérifier list size.

Veuillez trouver un exemple de code ci-dessous.

private void setupAdapter(){
    if (list.size() <= 10){
        recycler.setItemViewCacheSize(0);
     }
     recycler.setAdapter(adapter);
     recycler.setLayoutManager(linearLayoutManager);
}
0