[Android] Xử lí checkbox state trong recycler view

Trong phát triển Android, chúng ta thường phải xử lí các case về “check-select” trong một list items. Đối với list nhiều items thì RecyclerView là một lựa chọn tốt để giữ các view item này và có thể sử dụng lại các view item từ đó tăng thêm performance. Tuy nhiên khi recycler view thực hiện recycler các items này thì các trường giá trị của checkboxs, switchers … sẽ bị mất đi hoặc không chính xác đối với item đó. Để giải quyết việc này ta cần thực hiện xử lí logic đối với state của các check view đó (thường có 2 trạng thái checked và unchecked).

Đối với việc xử lí logic thì đơn giản ta chỉ cần đặt state của view đó vào trạng thái checked hoặc unchecked khi click vào, kiểm tra trạng thái trước đó và set giả trị ngược với giá trị hiện tại.

if (mCheckedView.isChecked()) {
    mCheckedView.setChecked(false);
}
else {
    mCheckedView.setChecked(true);
}

Về xử lí logic thì không có gì khó hiểu, tuy nhiên khi áp dụng trong recycler view, ta thực hiện scroll down, đối với các items bị ẩn khỏi màn hình, các view items mới sẽ sử dụng lại các view đó mà vẫn giữ lại state của check view của các item trước đó.

Để giải quyết vấn đề này ta có thể xem xét các giải pháp sau:

1. Không sử dụng lại View Holder

Trong view holder ta có thể set thuộc tính isRecyclerable false để ngăn recycler view không sử dụng lại view này. Ta thực hiện set ở constructor của view holder đó

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

Đây là một giải pháp tuy nhiên nó lại rất tệ bởi lý do chính ta sử dụng recycler view chính là khả năng sử dụng lại view của nó. Nếu sử dụng phương pháp này đối với list items nhiều sẽ tốn bộ nhớ để lưu giữ check state của từng item cũng như chính view này, từ đó ảnh hưởng đến performance.

2. Dùng model của item để lưu giữ state của view

Đây là cách phổ biến ta hay dùng. Ta có một biến boolean ở trong model item để giữ check state item đó.

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

Khi xử lí sự kiện onClick view ta chỉ cần set check cho item đó.

@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ới phương pháp này, mỗi lần recycler view thực hiện sử dụng lại view, ta chỉ việc lấy state của model đó ra và set cho check view, cách này đảm bảo không có sai lệch khi trạng thái đc gắn liền với model của item đó.

@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()));
        if (items.get(position).getChecked()) {
            mCheckedTextView.setChecked(true);
        }
        else {
            mCheckedTextView.setChecked(false);
        }
    }...

Phương pháp này rất dễ hiểu, tuy nhiên bất lợi của nó chính là giờ đây bản thân model phải quan tâm đến view. Thử tưởng tượng riêng model này nhưng cần nhiều check state ở từng view khác nhau (ví dụ checkboxs, switcher…) thì việc lưu giữ các biến state trong bản thân model này sẽ gây khó hiểu và tốn bộ nhớ lưu giữ cho các biến này.

3. Sử dụng một array để lưu giữ state items

Phương pháp này về cơ bản giống như phương pháp 2, tuy nhiên nó lại phân tách được nơi lưu trữ các biến view state của items ra để không ảnh hưởng đến model. Ta chỉ cần lưu array này ở trong adapter là đủ và cần bao nhiều array check state thì cũng chỉ cần khai báo trong adapter là xong.

Ta có thể sử dụng Map hoặc SparseBooleanArray (Giống như map nhưng mặc định key - value là kiểu int và boolean ) để lưu giữ view state trong list items. ta lưu giữ vi trí của item đó trong list làm key và value là trạng thái view state. Mỗi lần bind View ta chỉ việc get key để lây ra value view state items đó.

private SparseBooleanArray itemStateArray= new SparseBooleanArray();

Trong onClick() ta lấy view state từ Map ra và set cho view item

@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à check state view rồi update ở onBindViewHolder

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

@Override
public int getItemCount() {
    if (items == null) {
        return 0;
    }
    return items.size();
}

 void loadItems(List<Model> tournaments) {
    this.items = tournaments;
    notifyDataSetChanged();
}

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
      if (!itemStateArray.get(position, false)) {
            mCheckedTextView.setChecked(false);}
        else {
            mCheckedTextView.setChecked(true);
        }
}

Về cơ bản phương pháp này không khác phương pháp 2 nhưng nó phân tách được state view với model, giúp code được rõ ràng và model cũng gọn hơn 😃.


All Rights Reserved