• 20 hours
  • Hard

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 12/12/19

Interagissez avec la RecyclerView

Notre application NetApp est plutôt sympa ! Cependant, il serait bien que nos utilisateurs puissent interagir avec en cliquant par exemple sur un élément de la liste afin de réaliser des actions bien spécifiques.

Eh bien c'est ce que nous allons faire dans ce chapitre ! :p

Gérer le clic sur un élément

Reprenez votre application NetApp (disponible également à ce commit). Nous allons dans un premier temps implémenter le clic sur un élément de la liste.

Ce coup-ci, les développeurs d'Android ne nous ont pas vraiment facilité les choses... En effet, il n'y a pas de méthode comme  setOnClickListener(int position)  nous permettant de récupérer précisément le clic sur un élément de la liste...

Nous allons donc devoir faire autrement ! D'ailleurs, d'autres développeurs talentueux ont déjà proposé leurs solutions, comme on peut le voir sur cette question Stackoverflow devenue assez célèbre.

Nous allons dans notre cas utiliser la solution de Hugo Visser, que j'ai légèrement adaptée afin de la rendre un peu plus facile à utiliser. Ainsi, nous allons créer la classe ItemClickSupport.java dans le package Utils de notre application NetApp.

Classe Utils/ItemClickSupport.java :

public class ItemClickSupport {
    private final RecyclerView mRecyclerView;
    private OnItemClickListener mOnItemClickListener;
    private OnItemLongClickListener mOnItemLongClickListener;
    private int mItemID;
    private View.OnClickListener mOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (mOnItemClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
        }
    };
    private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            if (mOnItemLongClickListener != null) {
                RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
                return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
            }
            return false;
        }
    };
    private RecyclerView.OnChildAttachStateChangeListener mAttachListener
            = new RecyclerView.OnChildAttachStateChangeListener() {
        @Override
        public void onChildViewAttachedToWindow(View view) {
            if (mOnItemClickListener != null) {
                view.setOnClickListener(mOnClickListener);
            }
            if (mOnItemLongClickListener != null) {
                view.setOnLongClickListener(mOnLongClickListener);
            }
        }

        @Override
        public void onChildViewDetachedFromWindow(View view) {

        }
    };

    private ItemClickSupport(RecyclerView recyclerView, int itemID) {
        mRecyclerView = recyclerView;
        mItemID = itemID;
        mRecyclerView.setTag(itemID, this);
        mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
    }

    public static ItemClickSupport addTo(RecyclerView view, int itemID) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(itemID);
        if (support == null) {
            support = new ItemClickSupport(view, itemID);
        }
        return support;
    }

    public static ItemClickSupport removeFrom(RecyclerView view, int itemID) {
        ItemClickSupport support = (ItemClickSupport) view.getTag(itemID);
        if (support != null) {
            support.detach(view);
        }
        return support;
    }

    public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
        mOnItemClickListener = listener;
        return this;
    }

    public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
        mOnItemLongClickListener = listener;
        return this;
    }

    private void detach(RecyclerView view) {
        view.removeOnChildAttachStateChangeListener(mAttachListener);
        view.setTag(mItemID, null);
    }

    public interface OnItemClickListener {

        void onItemClicked(RecyclerView recyclerView, int position, View v);
    }

    public interface OnItemLongClickListener {

        boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
    }
}

Explications : Pas de panique, vous n'êtes absolument pas obligés de la comprendre ! >_< Cette classe utilitaire nous servira simplement à gérer plus facilement les clics de l'utilisateur sur la RecyclerView. Grosso Modo, celle-ci va créer des Listeners pour chaque élément (item) de notre RecyclerView, en prenant soin de les supprimer dès que ces derniers ne seront plus affichés dans la RecyclerView (pour éviter une trop grosse utilisation de la mémoire).

Implémentons maintenant cette classe dans notre contrôleur MainFragment.

Extrait de MainFragment.java :

public class MainFragment extends Fragment {
    
    ...

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        ...
        this.configureRecyclerView();
        this.configureSwipeRefreshLayout();
        // 2 - Calling the method that configuring click on RecyclerView
        this.configureOnClickRecyclerView();
        
        ...
    }

    ...

    // -----------------
    // ACTION
    // -----------------

    // 1 - Configure item click on RecyclerView
    private void configureOnClickRecyclerView(){
        ItemClickSupport.addTo(recyclerView, R.layout.fragment_main_item)
                .setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
                    @Override
                    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
                        Log.e("TAG", "Position : "+position);
                    }
                });
    }

    ...
}

Explications : Nous créons ici une méthode  configureOnClickRecyclerView()  qui nous permet de configurer le clic sur notre RecyclerView en utilisant la précédente classe créée, ItemClickSupport. Cette méthode sera bien évidemment appelée dans le  onCreateView()  de notre fragment.

Comme vous pouvez le voir,  grâce à la méthode  onItemClicked()  , nous allons pouvoir savoir sur quel élément de la liste l'utilisateur a cliqué, grâce à la variable position représentant l'index de la liste.

Représentation d'un index
Représentation d'un index

Maintenant que nous savons sur quel élément a cliqué l'utilisateur, grâce à la variable position (index), nous allons pouvoir retourner le bon objet (donc le bon GithubUser) correspondant. Pour cela, nous allons modifier notre Adapter afin de créer une méthode publique récupérant un utilisateur en fonction d'une position donnée.

Extrait de GithubUserAdapter.java :

public class GithubUserAdapter extends RecyclerView.Adapter<GithubUserViewHolder> {

    ...

    public GithubUser getUser(int position){
        return this.githubUsers.get(position);
    }
}

 Explications : Nous créons la méthode  getUser()  qui va retourner en fonction d'une position passée en paramètre, l'utilisateur de la liste correspondant. 

Il ne nous reste maintenant plus qu'à l'appeler dans notre fragment MainFragment.

Extrait de MainFragment.java :

public class MainFragment extends Fragment {
    
    ...
    
    private void configureOnClickRecyclerView(){
        ItemClickSupport.addTo(recyclerView, R.layout.fragment_main_item)
                .setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
                    @Override
                    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
                        // 1 - Get user from adapter
                        GithubUser user = adapter.getUser(position);
                        // 2 - Show result in a Toast
                        Toast.makeText(getContext(), "You clicked on user : "+user.getLogin(), Toast.LENGTH_SHORT).show(); 
                    }
                });
    }
    ...

    
}

Explications : Quand un clic se produit, la méthode  onItemClicked()  est automatiquement appelée. A l'intérieur de celle-ci, nous récupérerons le bon utilisateur (1) et afficherons son nom dans un Toast (2).

Exécutez votre application et vérifiez que lorsque vous appuyez sur un élément de la liste, le nom de l'utilisateur s'affiche bien dans un Toast... :)

Résultat
Résultat

Gérer le clic sur une vue de l'élément

Imaginons maintenant que nous souhaitions gérer le clic sur un bouton se trouvant DANS chaque élément (chaque ligne) de notre liste. Comment réaliser cela ?

Eh bien c'est ce que nous allons voir dès maintenant. Pour cela, créons un bouton "supprimer" sur l'ensemble des éléments de notre liste en modifiant notre fichier fragment_main_item.xml .

Extrait de fragment_main_item.xml :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="20dip">

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center">

        <ImageView
            android:id="@+id/fragment_main_item_image"
            android:layout_width="50dip"
            android:layout_height="50dip"
            android:layout_margin="5dip"/>

        <TextView
            android:id="@+id/fragment_main_item_title"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textSize="22sp"
            android:textStyle="bold"/>
            
        <!-- 1 | Add an image button -->
        <ImageButton
            android:id="@+id/fragment_main_item_delete"
            android:layout_width="30dip"
            android:layout_height="30dip"
            android:background="@android:drawable/ic_delete"/>

    </LinearLayout>

    ...

</LinearLayout>

Explications : Nous ajoutons ici une simple vue ImageButton qui nous permettra d'afficher une croix rouge sur chaque élément de notre liste.

Puis ensuite, nous allons devoir récupérer le clic sur cette ImageButton dans notre ViewHolder. Mais avant cela, nous allons créer une interface de callback dans notre Adapter qui nous permettra par la suite, de propager le clic de l'utilisateur du ViewHolder vers le Fragment.

Représentation de l'interface de Callback
Représentation de l'interface de Callback

Extrait de GithubUserAdapter.java :

public class GithubUserAdapter extends RecyclerView.Adapter<GithubUserViewHolder> {

    // 1 - Create interface for callback
    public interface Listener {
        void onClickDeleteButton(int position);
    }

    // 2 - Declaring callback
    private final Listener callback;

    ...

    // 3 - Passing an instance of callback through constructor
    public GithubUserAdapter(List<GithubUser> githubUsers, RequestManager glide, Listener callback) {
        ...
        this.callback = callback;
    }

    ...

    // 4 - Passing an instance of callback through each ViewHolder
    @Override
    public void onBindViewHolder(GithubUserViewHolder viewHolder, int position) {
        viewHolder.updateWithGithubUser(this.githubUsers.get(position), this.glide, this.callback);
    }

    ...
}

Explications : Nous avons ici défini une interface de callback qui nous permettra de propager plus facilement le clic de l'utilisateur du ViewHolder vers le fragment.

  • Ligne 1 : Nous créons une interface de callback contenant une méthode,  onClickDeleteButton(int position)  , qui sera déclenchée par le ViewHolder dès qu'un utilisateur appuiera sur l'ImageButton de suppression. 

  • Ligne 2 : Nous déclarons une instance de la précédente interface.

  • Ligne 3 : Nous passons l'instance implémentée et créée par la suite dans le fragment MainFragment via le constructeur de l'Adapter.

  • Ligne 4 : Et enfin, nous passons l'instance de cette interface de callback à chaque ViewHolder depuis la méthode  updateWithGithubUser()

Implémentons maintenant les changements correspondants dans le ViewHolder.

Extrait de GithubUserViewHolder.java :

public class GithubUserViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener  {

    ...
    // 1 - Declare our ImageButton
    @BindView(R.id.fragment_main_item_delete) ImageButton imageButton;

    // 2 - Declare a Weak Reference to our Callback
    private WeakReference<GithubUserAdapter.Listener> callbackWeakRef;

    ...

    public void updateWithGithubUser(GithubUser githubUser, RequestManager glide, GithubUserAdapter.Listener callback){
        
        ...

        //3 - Implement Listener on ImageButton
        this.imageButton.setOnClickListener(this);

        //4 - Create a new weak Reference to our Listener
        this.callbackWeakRef = new WeakReference<GithubUserAdapter.Listener>(callback);
    }

    @Override
    public void onClick(View view) {
        // 5 - When a click happens, we fire our listener.
        GithubUserAdapter.Listener callback = callbackWeakRef.get();
        if (callback != null) callback.onClickDeleteButton(getAdapterPosition());
    }
}

Explications : Nous implémentons dans le ViewHolder le callback précédemment créé et instancié, et nous faisons en sorte de le déclencher dès qu'un utilisateur clique sur le bouton de suppression.

  • Ligne 1 : Nous déclarons la vue précédemment créée dans le XML

  • Ligne 2 : Nous déclarons une WeakReference de notre callback, afin de permettre au Garbage Collector de supprimer celui-ci au besoin et éviter ainsi des MemoryLeaks.

  • Ligne 3 : On implémente un listener à notre ImageButton de suppression. On oublie également pas d'ajouter  implements View.OnClickListener  à notre classe, et re-déclarer ainsi la méthode  onClick(View view)  .

  • Ligne 4 : On crée ici une référence faible (WeakReference) de notre callback afin de ne pas créer de MemoryLeaks.

  • Ligne 5 : Et enfin nous appelons la méthode  onClickDeleteButton()  de ce callback afin de transmettre la communication jusqu'à notre contrôleur MainFragment. 

Passons maintenant à la souscription de ce callback par notre fragment MainFragment.

Extrait de MainFragment :

public class MainFragment extends Fragment implements GithubUserAdapter.Listener {

    // 1 - Implement the callback interface
    ...
    
    // -----------------
    // ACTION
    // -----------------

    ...
    
    // 2 - Because of implementing the interface, we have to override its method

    @Override
    public void onClickDeleteButton(int position) {
        GithubUser user = adapter.getUser(position);
        Toast.makeText(getContext(), "You are trying to delete user : "+user.getLogin(), Toast.LENGTH_SHORT).show();
    }
    
    ...
    
     private void configureRecyclerView(){
        ...
        // 3 - Passing reference of callback
        this.adapter = new GithubUserAdapter(this.githubUsers, Glide.with(this), this);
        ...
    }
    
    ...
    
}

Explications : Nous devons ici implémenter l'interface de callback (1) précédemment créée, afin de pouvoir correctement récupérer le clic sur une vue se trouvant à l'intérieur d'un ViewHolder (en l'occurrence notre ImageButton).

Une fois que nous avons re-déclaré (2) la méthode  onClickDeleteButton(int position) , nous pouvons enfin récupérer (via l'Adapter, grâce à la variable position) l'utilisateur en questionOn n'oublie également pas de passer une instance de notre fragment qui implémente cette interface à notre Adapter (3).

Lancez votre application et effectuez le test en cliquant sur le bouton de suppression de n'importe quel élément. Parfait, tout fonctionne ! :D

Résultat
Résultat
Example of certificate of achievement
Example of certificate of achievement