Cải thiện hiển thị PDF trong app bằng cách sử dụng pagination Android

việc hiển thị và tương tác với các tài liệu PDF lớn là một thách thức không nhỏ, đặc biệt khi các tệp này có nhiều trang và dung lượng lớn. Một giải pháp hiệu quả để cải thiện trải nghiệm người dùng là sử dụng pagination Android, giúp giảm đáng kể thời gian tải và tối ưu hóa việc hiển thị.

Bài viết này sẽ chia sẻ cách chúng tôi đã áp dụng pagination Android để cải thiện tốc độ hiển thị PDF lên đến 70-80%, mang lại trải nghiệm mượt mà hơn cho người dùng.

Cải thiện hiển thị PDF trong app bằng cách sử dụng pagination Android

Học hỏi từ những sai lầm

Ban đầu, chúng tôi lấy liên kết S3 (liên kết lưu trữ PDF) từ máy chủ và sau đó tải xuống tệp PDF bằng API Download Manager.

Sau khi tải xuống hoàn tất, chúng tôi chuyển đổi từng trang của PDF đã tải thành hình ảnh bằng cách sử dụng thư viện PdfRenderer và sau đó hiển thị tệp PDF cho người dùng dưới dạng danh sách hình ảnh bằng cách sử dụng RecyclerView.

Trong cách tiếp cận trước đây, chúng tôi đã chuyển đổi tất cả các trang của PDF thành hình ảnh cùng một lúc trước khi điền danh sách bằng các hình ảnh.

Vấn đề với phương pháp này là hàm được sử dụng để tạo bitmap cho mỗi trang tiêu tốn nhiều tài nguyên và thời gian, mất khoảng 5 giây cho mỗi trang.

Kết quả là, đối với các tệp PDF lớn có nhiều trang, quá trình chuyển đổi tất cả các trang thành hình ảnh và tải danh sách hình ảnh vào RecyclerView mất rất nhiều thời gian, dẫn đến việc tải báo cáo chậm và ảnh hưởng tiêu cực đến trải nghiệm người dùng.

Làm thế nào cải thiện tốc độ?

Chúng tôi nhận ra rằng không cần thiết phải tải tất cả các trang của PDF cùng một lúc, vì trên thực tế, chỉ có hai trang được hiển thị trên màn hình di động tại bất kỳ thời điểm nào. Vì vậy, việc tải trước tất cả các trang là không cần thiết.

Thách thức là phải tạo logic phân trang riêng của chúng tôi. Để làm điều này, chúng tôi đã sử dụng Paging Library của Android và phát triển một lớp datasource tùy chỉnh.

Dưới đây là lớp datasource tùy chỉnh mà chúng tôi đã tạo để triển khai logic phân trang của mình.

class ReportDataSource(
    private val file: MutableLiveData<File>,
    private val loading: MutableLiveData<Boolean>,
    private val httpException: MutableLiveData<HttpException?>
) : BasePageKeyedDataSource<String>(httpException) {

    private val pdfRenderer by lazy {
        PdfRenderer(
            ParcelFileDescriptor.open(file.value, ParcelFileDescriptor.MODE_READ_ONLY)
        )
    }

    val factory = object : DataSource.Factory<Int, String>() {
        override fun create() = ReportDataSource(file, loading, httpException)
    }

    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, String>
    ) {
        if (file.value != null) {
            try {
                if (pdfRenderer.pageCount > 0) {
                    callback.onResult(listOf(getBitmap(0)), null, 1)
                } else {
                    callback.onResult(emptyList(), null, 1)
                }
            } catch (e: Throwable) {
                callback.onResult(emptyList(), null, 1)
            }
            loading.postValue(false)
        }
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, String>) {
        try {
            if (params.key < pdfRenderer.pageCount) {
                callback.onResult(listOf(getBitmap(params.key)), params.key - 1)
            } else {
                callback.onResult(emptyList(), params.key - 1)
            }
        } catch (e: Throwable) {
            callback.onResult(emptyList(), params.key - 1)
        }
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, String>) {
        try {
            if (params.key < pdfRenderer.pageCount) {
                callback.onResult(listOf(getBitmap(params.key)), params.key + 1)
            } else {
                callback.onResult(emptyList(), params.key + 1)
            }
        } catch (e: Throwable) {
            callback.onResult(emptyList(), params.key + 1)
        }
    }

    private fun getBitmap(page: Int): String? {
        // Logic để chuyển đổi PDF trang thành hình ảnh bitmap
        return null
    }
}

Phân tích định lượng

Để đưa vào số liệu cụ thể về việc chúng tôi đã cải thiện tốc độ hiển thị PDF như thế nào, giả sử thời gian thực hiện của hàm getBitmap(page: Int) (chuyển đổi một trang PDF thành hình ảnh) là T.

Hàm này là thời gian tiêu tốn nhất và quyết định thời gian hiển thị PDF. Giả sử tệp PDF của bạn có P trang.

Trước đây, thời gian tải PDF là P*T, vì tất cả các trang đều được tải cùng một lúc. Với phân trang, bây giờ chỉ mất 2*T, vì chỉ có hai trang được tải tại một thời điểm. Điều này có nghĩa là chúng tôi đã giảm thời gian đi (P-2)*T.

Tính theo phần trăm, mức giảm thời gian hiển thị PDF là:

(P-2)T / PT = (P-2) / P %

Trong đó:

  • P = số trang trong PDF
  • T = thời gian thực hiện hàm getBitmap(page)

Sau một số thử nghiệm, chúng tôi nhận thấy rằng hàm getBitmap() thường mất 5 giây để thực hiện. Vì vậy, T = 5 giây.

Đối với một tệp PDF có P = 10 trang, phương pháp cũ sẽ mất 50 giây (P*T), trong khi phương pháp phân trang mới của chúng tôi chỉ mất 10 giây.

Như bạn có thể thấy, chúng tôi đã giảm thời gian tải xuống 80%.

Và đó, các bạn ạ, là sức mạnh của pagination Android.

Để tìm hiểu thêm về phân trang, bạn có thể truy cập hướng dẫn chính thức.