Tối ưu việc load ảnh với RxJava

964

1. Context

Để tạo một ứng dụng với trải nghiệm người dùng tuyệt vời, điều quan trọng là giảm thiểu thời gian người dùng chờ load dữ liệu.Đối với việc hiển thị hình ảnh, trong hầu hết các trường hợp, để tăng UX ta thường tải ảnh có chất lượng thấp (ảnh thumbnail) trước hình ảnh có chất lượng tốt hơn (ảnh original) được hiển thị là để user không phải chờ đợi việc load ảnh gốc với dung lượng lớn

Trong bài này, mình muốn chỉ cho bạn cách triển khai tải hình ảnh lũy tiến trong ứng dụng Android bằng RxJava và Kotlin.

2. Fetching multiple images

Nếu bạn đã sử dụng Picasso, bạn chắc chắn đã biết cách load ảnh và show chúng lên imageview đơn giản như thế này:

Thật đáng tiếc rằng, method này không thể sử dụng khi ảnh được lấy từ 2 nguồn URLs để load lên 1 imageview Khi gọi load(urlOri).into(imageView) lên imageview khiến cho load(urlThumb).into(imageView) đã bị cancel

Đó là lý do tại sao mỗi image phải được tải vào mục riêng biệt,nó là nơi mà bitmap sẽ được đổ vào ImageView khi được fetched.

Tại sao lại như vậy? Bạn có thể vào source code của Picasso và thấy:Để bảo vệ các target không bị Garbage Collection thu dọn nên cần giữ một tham chiếu đến nó. Một cách để xử lí việc này là tạo một MutableList<Target> và lưu trữ các target trong đó cho đến khi bitmap được fetch hoàn tất.

3. Demo

Ở đây mình làm ví dụ load ảnh trên trang “https://picsum.photos” với url là https://picsum.photos/{width}/{height}/?image=0 trong đó width, height tương ứng là chiều dài và chiều rộng của bức ảnh.

Ý tưởng là chuyển url với các chất lượng ảnh khác nhau vào phương thức và nhận sự kiện mỗi lần hình ảnh với chất lượng tốt hơn được nhận. Khi đã nhận về bitmap sẽ được lưu trữ trong LiveData được observed ra view để hiển thịViewModel sẽ gửi các error nếu hoàn tất và không có image nào được load thành công.

Sau đó view chỉ cần lắng nghe để hiển thị ảnh bằng Bitmap đươc phát ra từ viewmodel nếu thành công, và show error nếu xảy ra lỗi

Có thể bạn quan tâm

  Tối ưu hoá MySQL sử dụng việc gộp các index
  Một số tối ưu cho Sublime text - Code tiện hơn!

3.1 Use case 2 URLs

Trong trường hợp đơn giản nhất chỉ tìm nạp hai url cùng lúc, điều duy nhất cần được thực hiện là tạo ra các observable cho mỗi url với mỗi loại ảnh và merge chúng lại với nhau.

Trong method loadImageAndIgnoreError, ta đã tạo duy nhất từ ​​cá thể lớp thực hiện giao diện SingleOnSubscribe <BitmapWithQuality> và sau đó biến nó thành observable

Khi có lỗi xảy ra thì sẽ phát ra Observable.empty<BitmapWithQuality>() để hiển thị error lên view khi tất cả các lời gọi hàm được hoàn tất

3.2. Multiple URLs

Vậy làm thế nào để load đồng thời nhiều URLs, đối với trường hợp này , ta sẽ sử dụng kết hợp các toán tử map, merge và reduce.

Đầu tiên lấy list các url và map chúng thành Pair<url, quality>. Sau đó, sử dụng toán tử map mà ta đã Observalbe được từ cặp đó bằng cách sử dụng loadImageAndIgnoreError(), giống như trên kia. Bước cuối cùng là sử dụng reduce để hợp nhất tất cả các observable với nhau và lấy tất cả các image cùng một lúc.

Việc sử dụng reduce là rất hợp lí trong trường hợp này. Nó áp dụng một hàm cho mỗi mục được phát ra bởi một Observable tuần tự và phát ra giá trị cuối cùng. Trong trường hợp này giá trị cuối cùng là giá trị observable được tạo ra từ việc hợp nhất tất cả các observable thành phần với nhau.

3.3. ImageFetcherSingleSubscribe

ImageFetcherSingleSubscribe là một class implement interface SingleOnSubscribe được override method subscribe nhận về một SingleEmitter<BitmapWithQuality> cho phép handler chúng.

Trong subscribe method , tạo class CustomImageLoadTarget như một nguồn phát, sẽ phát ra Bitmap nếu thành công và phát ra exception nếu lỗi, sau đó unsubscribe để cancelRequest và xóa nó ra khỏi list runningTarget

Tiếp theo, thêm các target vào mutable list để ngăn việc bị Garbage Collection thu dọn cho đến khi lời gọi hàm kết thúc. Sau đó, đích đến được chuyển cho Picasso để hiển thị

3.4. CustomImageLoadTarget

CustomImageLoadTarget là một class implementing Target interface mà instance đó sẽ được chuyển vào method

Trong init, gọi emitter.setCancellable {unSubscribe (this)} để unSubscribe và target bị xóa khỏi list sau khi emitter được xử lý.

Khi bitmap được tìm nạp trong method onBitmapLoaded emitter.onSuccess () được gọi với bitmap đã nạp và chất lượng của nó. Khi tìm nạp bitmap thất bại trong onBitmapFailed emitter.tryOnError() được gọi. Sau khi phát ra một trong hai thành công hoặc một unSubscribe lỗi phải được gọi để loại bỏ target khỏi map và loại bỏ tham chiếu để nó có thể được GC thu dọn

4. Load thumb với Glide

Glide cũng đã hỗ trợ việc load Thumbnail rất đơn giản:

Bạn cũng có thể dùng mới Multi Urls với cách lầy như thế này

Nhược điểm của phương pháp này là bạn không thể áp dụng multi Url phức tạp hoặc logic xử lý tiến trình. Phương pháp đầu tiên mà ta thực hiện bằng cách sử dụng RxJava cho phép kiểm soát nhiều hơn trạng thái hơn nhưng nhược điểm của nó là khá phức tạp.

TopDev via Viblo

  Top 10 câu hỏi phỏng vấn Java thường gặp
  4 tính chất của lập trình hướng đối tượng trong Java