Xử lý multi items choice trong Recycler view

Trong khi làm project, sẽ có lúc bạn cần phải xử lý việc lựa chọn nhiều item (multi items choice) từ một list các item. Khi mà số lượng item tăng lên thì một lựa chọn tốt hơn là sử dụng recycler view để chứa các item vì recycler view có cơ chế tái sử dụng (recycle) view nên cho hiệu năng tốt hơn. Nhưng một khi mà recycler view recycle các item của nó thì những view mà có trạng thái toggle (ví dụ on/off của switch, check/uncheck của checkbox, …) thường chạy một cách khó hiểu với recycler view, nhất là khi bạn không tự xử lý logic việc hiển thị trạng thái (state) của các view.

Cách phổ biến mà một dev có thể thiết lập trạng thái của một view là set nó thành checked khi view được click và bỏ check khi view được clcik lại một lần nữa bằng việc kiểm tra trạng thái trước đó của view như đoạn code sau:

mCheckedView.setChecked(!mCheckedView.isChecked()));

Logic này thoạt đầu trông có vẻ đúng đấy, nhưng không hẳn đâu nhé, khi bạn thực hiện scroll recycler view xuống đến khi mà không còn nhìn thấy item đó nữa, lúc đó thì recycler view sẽ dùng lại trạng thái của item đó để set cho item khác trong list. Điều này cũng xảy ra khi bản phím xuất hiện và che các item rồi ẩn đi. Hãy cùng nhìn hình minh họa sau:

Vậy làm cách nào để chúng ta xử lý vấn đề này đây?

Chúng ta cùng xem xét các phương án sau

  1. Không cho phép view holder tái sử dụng view. (KHÔNG NÊN DÙNG)
  2. Sử dụng model để lưu state của các item.
  3. Sử dụng một array để lưu state của các item.

Chúng ta cùng đi vào phân tích từng phương án nhé

1. Không cho phép view holder tái sử dụng view.

Bạn có thể thực hiện việc này bằng cách set thuộc tính isRecycleable của view holder trong recycler view thành false. Việc này sẽ chặn recycler view tái sử dụng các view của nó. Dưới đây là code minh họa:

ViewHolder(View itemView) {
    super(itemView);
    ...
    this.setIsRecyclable(false);
}

Đây là một ý kiến rất tồi vì chúng ta đang sử dụng recycler view vì ưu điểm của việc tái sử dụng view. Lựa chọn này có thể giải quyết hiển thị sai trạng thái của các view nhưng một khi các view không còn hiển thị thì chúng sẽ trở về trạng thái mặc định.

Do đó nên chúng ta sẽ cần một chút bộ nhớ để có thể lưu trạng thái hiện tại của từng item trong recycler view.

2, Sử dụng model để lưu state của các item.

Bạn có thể thêm một trường boolean vào model mà bạn sử dụng với recycler view để có thể lưu state hiện tại của item như sau:

private boolean isChecked;

public boolean getChecked() {
    return isChecked;
}

public void setChecked(boolean checked) {
    isChecked = checked;
}

Và khi mà item được click thì bạn có thể thay đổi state của item như sau:

@Override
public void onClick(View v) {
   // toggle the checked view based on the checked field in the model
    int adapterPosition = getAdapterPosition();
    if (items.get(adapterPosition).getChecked()) {
        mCheckedTextView.setChecked(false);
        items.get(adapterPosition).setChecked(false);
    }
    else {
        mCheckedTextView.setChecked(true);
        items.get(adapterPosition).setChecked(true);
    }
}

Và chúng ta cần đảm bảo rằng state đúng sẽ được hiển thị khi recycler view bind các view holder như sau:

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
    holder.bind(position);
}

class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

    CheckedTextView mCheckedTextView;

    ViewHolder(View itemView) {
        super(itemView);
        mCheckedTextView = (CheckedTextView) itemView.findViewById(R.id.checked_text_view);
        itemView.setOnClickListener(this);
    }

    void bind(int position) {
        mCheckedTextView.setText(String.valueOf(items.get(position).getPosition()));
        mCheckedTextView.setChecked(items.get(position).getChecked());
    }...

Như vậy thì các view sẽ hiển thị đúng state của nó rồi.

Khuyết điểm của cách này là model có biết về view (nghĩa là model có phụ thuộc vào view chứ không độc lập hoàn toàn) và đây không phải là thiết kế tốt cho lắm. Vì thế mà phương án cuối cùng sẽ là lưu các state của view ở trong adapter.

3, Sử dụng một array để lưu các state của các item.

Để làm việc này thì chúng ta sẽ dùng một Map hoặc một SparseBooleanArray (tương tự như Map nhưng có key và value theo cặp là int và boolean) để lưu trạng thái của tất cả các item trong list, và sử dụng các key và value để so sánh khi thực hiện thay đổi trạng thái.

Chúng ta tạo một SparseBooleanArray trong recycler view adapter như sau

// sparse boolean array for checking the state of the items
    private SparseBooleanArray itemStateArray= new SparseBooleanArray();

Tiếp đó xử lý sự kiện onClick() của các item, chúng ta sẽ sử dụng state của các item ở trong itemStateArray trước khi thay đổi trạng thái của các item như sau:

        @Override
        public void onClick(View v) {
            int adapterPosition = getAdapterPosition();
            if (!itemStateArray.get(adapterPosition, false)) {
                mCheckedTextView.setChecked(true);
                itemStateArray.put(adapterPosition, true);
            }
            else  {
                mCheckedTextView.setChecked(false);
                itemStateArray.put(adapterPosition, false);
            }
        }

Và chúng ta cũng sử dụng sparse boolean array để set state cho các view khi view được bind như sau:

@Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.bind(position);
    }

    class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        CheckedTextView mCheckedTextView;

        ViewHolder(View itemView) {
            super(itemView);
            mCheckedTextView = (CheckedTextView) itemView.findViewById(R.id.checked_text_view);
            itemView.setOnClickListener(this);
        }

        void bind(int position) {
            // use the sparse boolean array to check
            mCheckedTextView.setChecked(itemStateArray.get(position, false));
            }
        }

Cách này có ưu điểm là mọi thứ đều được xử lý gọn ở trong adapter mà không cần động chạm gì tới model.

Từ hình trên bạn đã thế rằng state checked/unchecked của các view được giữ nguyên ngay cả khi mà chúng đã được tái sử dụng (recycled).

Một điều nữa là nếu bạn cần làm một single item choice với một list sử dụng recycler view thì bạn chỉ cần duy nhất một biến int trong recycler view để có thể lưu position của item được checked là đủ.


public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder>    

    private int mSelectedPosition = -1;

    public class MyViewHolder extends RecyclerView.ViewHolder {

        private CheckedTextView mCheckedTextView;

        void onClick() {
            mSelectedPosition = getAdapterPosition();
        }

        void bind(int position) {
            mCheckedTextView.setChecked(position == mSelectedPosition);
        }

Tham khảo: android.jlelse.eu