Pobieranie pozycji elementu w adapterze RecyclerView - getBindingAdapterPosition()

Przygotowując się na jedno ze szkoleń z programowania na Androida, natknąłem się na pewną sytuację podczas pisania adaptera dla RecyclerView. Projekt aplikacji mobilnej jest z założenia bardzo prosty: najprostsza aplikacja listy zadań do zrobienia (lista TODO) z możliwością oznaczania zadań jako wykonane oraz ich usuwania. Jednak podczas tak prostej czynności, jaką jest usuwanie elementu z listy, może pojawić się problem z pobraniem niewłaściwej jego pozycji. Czy wiesz, w jaki sposób pobrać właściwą pozycję dla elementu listy?

Każdy adapter dla RecyclerView musi implementować metodę onBindViewHolder(), która może wyglądać w następujący sposób:

@Override
public void onBindViewHolder(@NonNull ListItemHolder holder, int position) {

}

Jak widać, jako drugi parametr jest zwracana pozycja elementu listy, który w danej chwili jest zawiązywany. Z tego punktu widzenia sprawa wydaje się prosta więc co jeśli wykorzystamy ten parametr do usuwania elementu z listy jak w ten sposób:

@Override
public void onBindViewHolder(@NonNull final ListItemHolder holder, final int position) {
    holder.bDelete.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            listItems.remove(position);
            notifyItemRemoved(position);
        }
    });
}

W takim przypadku wewnątrz metody onClick() pozycja elementu niestety nie będzie już właściwa i w ten sposób możemy usunąć zupełnie inny element niż ten, dla którego przycisk został kliknięty. Dzieje się tak, ponieważ RecyclerView nie aktualizuje pozycji dla już zawiązanych pozycji, ale tylko dla nowych i może być tak, że po kliknięciu przycisku usuwania piątego elementu, zostanie usunięty element, który znajduje się na pozycji zerowej.

Jak więc właściwie poradzić sobie z tym problemem? Otóż klasa RecyclerView.ViewHolder dostarcza nam metodę getBindingAdapterPosition().

Przykład użycia getBindingAdapterPosition()

Sposób działania tej metody jest następujący: po jej wywołaniu następuje odpytanie do adaptera o najbardziej zaktualizowaną pozycję dla uchwytu (holdera), dzięki czemu dostajemy zawsze aktualną pozycję względem adaptera:

@Override
public void onBindViewHolder(@NonNull final ListItemHolder holder, int position) {
    holder.bDelete.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            int itemPosition = holder.getBindingAdapterPosition();

            listItems.remove(itemPosition);
            notifyItemRemoved(itemPosition);
        }
    });
}

Należy przy tym zwrócić uwagę, że powyższe zastosowanie będzie się odnosiło do powiadomienia adaptera o zmianach poprzez użycie metod notifyItem*(), ponieważ notifyDataSetChanged() sprawi, że cała lista ulegnie przewiązaniu i wówczas getBindingAdapterPosition() może zwrócić pozycję o wartości -1, czyli NO_POSITION (również w przypadku, gdy element nie znajduje się już na liście). Jeśli pojawi się u nas taka sytuacja, wówczas najlepiej jest zignorować kliknięcie, ponieważ nie wiemy, co powinniśmy z nim zrobić.