+1

RXJava và DiffUtil

Nếu bạn đang sử dụng RecyclerView và phải thường xuyên update dữ liệu từ API khi thay đổi, bạn có thể đã dùng class DiffUtil. Tiện ích tuyệt vời này giúp bạn tạo ra một loạt call đến notifyItemInserted (), notifyItemRemoved (), ... trên adapter bằng cách đơn giản so sánh phiên bản hiện tại của dữ liệu với phiên bản mới. Tất cả bạn cần làm là implement callback mà không so sánh các yếu tố từ danh sách hiện tại và mới nhất.

private static class MyDiffCallback extends DiffUtil.Callback {
  private List<Thing> current;
  private List<Thing> next;

  public MyDiffCallback(List<Thing> current, List<Thing> next) {
    this.current = current;
    this.next = next;
  }

  @Override
  public int getOldListSize() {
    return current.size();
  }

  @Override
  public int getNewListSize() {
    return next.size();
  }

  @Override
  public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
    Thing currentItem = current.get(oldItemPosition);
    Thing nextItem = next.get(newItemPosition);
    return currentItem.getId() == nextItem.getId();
  }

  @Override
  public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
    Thing currentItem = current.get(oldItemPosition);
    Thing nextItem = next.get(newItemPosition);
    return currentItem.equals(nextItem);
  }
}
DiffResult diffResult = DiffUtil
  .calculateDiff(new MyDiffCallback(current, next), true);
diffResult.dispatchUpdatesTo(adapter);

Đoạn mã trên cho thấy cách implement callback, cách thực hiện tính toán và cách gửi các cuộc gọi thông báo tới RecyclerView adapter.

Tuy nhiên, có một thách thức với việc sử dụng này. Nếu dữ liệu của bạn là rất lớn, hoặc so sánh bạn làm là rất phức tạp, bạn nên tránh gọi tính toán DiffUtil trên main thread. Di chuyển này đến một background thread giải quyết vấn đề và sau đó bạn có thể gửi kết quả trong một message đến main thread, nơi nó được gọi trên adapter sau khi đã thiết lập các dữ liệu mới.

Bây giờ đây là nơi mà mọi thứ được tricky. Bởi vì tính toán DiffUtil cần cả danh sách hiện có và danh sách dữ liệu mới, ta cần phải truy cập dữ liệu hiện có trong background. Nếu adapter có phương thức setData (), nó sẽ cần một getData (). Điều này có nghĩa là dữ liệu sẽ được truy cập từ nhiều thread, do đó có thể sẽ cần một số đồng bộ hóa hoặc một cấu trúc dữ liệu an toàn. Làm thế nào nếu chúng ta có thể tránh được điều này?

RxJava, giải pháp huyền diệu của chúng ta cho hầu hết mọi thứ. Giả sử bạn có một Flowable <List <Thing >> listOfThings, sẽ emit phiên bản mới nhất của dữ liệu mà RecyclerView sẽ hiển thị. Chúng ta có thể đăng ký trên một IO hoặc computation scheduler để đảm bảo rằng bất cứ công việc bị chặn thực hiện trên main thread, và sau đó observe các sự kiện trên main thread, nơi chúng ta chỉ cần truyền dữ liệu tới adapter.

ThingRepository
    .latestThings(2, TimeUnit.SECONDS)
    .subscribeOn(computation())
    .observeOn(mainThread())
    .subscribe(things -> {
      adapter.setThings(things);
      adapter.notifyDataSetChanged();
    });

Đoạn mã ở trên cho thấy làm thế nào để đảm bảo emisstion tốn kém của danh sách dữ liệu mới được thực hiện trên computation thread và làm thế nào chúng ta chuyển sang main thread trong subscirption, nơi chúng ta gọi notifyDataSetChanged () trên adapter. Nó hiệu quả, nhưng sẽ không hiển thị tốt vì mọi thứ đều được vẽ lại cho mỗi danh sách mới (đó là lý do tại sao bạn muốn gọi các phương thức thông báo khác).

Để sử dụng DiffUtil, chúng ta cần phải gọi phương thức calculateDiff () và vượt qua DiffResult cùng với danh sách mới nhất <Thing> trong subscription. Một cách dễ dàng để làm điều này là sử dụng lớp Pair, là một thực hiện đơn giản nhưng mạnh mẽ của một bộ trong thư viện hỗ trợ. Vì vậy, chúng ta sẽ thay đổi subscription để nhận được một sự kiện kiểu Pair <List<Thing>, DiffResult>.

.subscribe(listDiffResultPair -> {
  List<Thing> nextThings = listDiffResultPair.first;
  DiffUtil.DiffResult diffResult = listDiffResultPair.second;
  adapter.setThings(nextThings);
  diffResult.dispatchUpdatesTo(adapter);
});

.scan()

Callback được gửi qua DiffUtil.calculateDiff () cần cả danh sách hiện tại và danh sách mới để làm việc. Làm thế nào chúng ta có thể chắc chắn rằng đối với mỗi danh sách mới nhận được từ source, chúng ta cũng muốn có sự kiện trước đó? Đây là một trong những thời điểm mà RxJava cho thấy nó quyền lực thực sự. Một trong những operator bí ẩn khác trong RxJava là scan (). Scan sẽ cung cấp cho bạn các next và previous event và pass nó trong chức năng của bạn, nơi bạn trả lại các sự kiện tiếp theo sẽ được thông qua. Phương pháp này có ba biến thể, và một trong chúng ta muốn là phiên bản thứ hai cũng có một giá trị ban đầu. Giá trị ban đầu của chúng ta để scan () sẽ là một cặp gồm một danh sách trống và null như là giá trị DiffResult.

Bây giờ chúng ta gọi calculateDiff () và cho nó danh sách trong Pair (danh sách trước đó) và danh sách tiếp theo. Chúng ta sử dụng kết quả để xây dựng một cặp mới với danh sách tiếp theo và DiffResult mới của chúng tôi. Lưu ý rằng chúng ta không sử dụng DiffResult trong cặp đầu vào trong phương pháp này.

Chúng ta có thêm một điều để suy nghĩ. Nếu chúng ta để nó như thế này, sự kiện đầu tiên sẽ chứa một cặp mà DiffResult là null (từ giá trị ban đầu), vì vậy chúng ta phải thực hiện một kiểm tra null trong đăng ký. Tuy nhiên, vì sự kiện này cũng là danh sách trống như chúng ta đã bắt đầu, chúng ta có thể bỏ qua bằng cách sử dụng toán tử skip (1). Điều này sẽ bỏ qua sự kiện đầu tiên.

List<Thing> emptyList = new ArrayList<>();
adapter.setThings(emptyList);
Pair<List<Thing>, DiffUtil.DiffResult> initialPair = Pair.create(emptyList, null);
ThingRepository
  .latestThings(2, TimeUnit.SECONDS)
  .scan(initialPair, (pair, next) -> {
    MyDiffCallback callback = new MyDiffCallback(pair.first, next);
    DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);
    return Pair.create(next, result);
  })
  .skip(1)

Điều gì sẽ xảy ra nếu mọi thứ trở nên quá nhanh?

Có một cảnh báo cuối cùng với cách tiếp cận này. Điều gì sẽ xảy ra nếu các sự kiện được phát ra nhanh hơn chúng ta có thể consume? Điều gì nếu tính toán DiffUtil mất vài giây và chúng ta nhận được bộ dữ liệu mới nhanh hơn? Ở đây bạn phải thực hiện một số cân nhắc về áp lực trở lại của RxJava. Với RxJava 2, bạn có thể sử dụng Flowable thay vì Observable và tùy chọn cấu hình back pressure phù hợp với tình huống của bạn. Trong trường hợp này, tôi chỉ đệm tất cả sự kiện để đảm bảo mọi thứ được xử lý và hiển thị. Nếu sự kiện xảy ra quá nhanh, ta có thể cân nhắc sử dụng một cái gì đó như debounce () để đảm bảo rằng không cập nhật adapter quá thường xuyên. Tất cả điều này phụ thuộc vào trường hợp sử dụng cụ thể, nhưng các operator RxJava và các chiến lược áp lực trở lại sẽ giúp bạn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí