Skip to main content
· 5 min read

Improve PDF rendering in Android app using Pagination

BlogPostImage

PDF files have become an essential part of our digital lives, and thus, the ability to view and navigate these documents seamlessly within apps has gained significant importance. One common problem that developers face is the slow rendering of large PDF files, which can negatively impact user experience. These files can be in form of financial reports or simply speaking progress reports in many productivity apps.

Not long ago at Powerplay, we received numerous comments from users who were experiencing slow loading times for their progress reports.

To provide some background, a progress report is a PDF file containing details about a user's project.

In response to this issue we set out to significantly enhance the rendering speed and managed to reduce the rendering time by an impressive 70-80%.

In this article, I'll share the steps we took to achieve this remarkable improvement.

I'll begin by highlighting the errors we initially made and then explain how we corrected them.

Learning from our errors

We initially obtained the S3 link (which is our PDF storage link) from the server, and then downloaded the PDF using the Download Manager API. Once the download was complete, we converted each page of the downloaded PDF into images using the Pdf Renderer library, and then displayed the PDF to users as a list of images using a recycler view.

In our previous approach, we converted all the PDF pages into images at once before populating the list with the images.

The issue with this method was that the function used to generate the bitmap of each page was resource-intensive and time-consuming, taking roughly 5 seconds per page. As a result, for larger PDFs with multiple pages, the process of converting all the pages into images and loading the image list into the recycler view took a significant amount of time. This led to slow report loading times and negatively impacted the user experience.

How did we enhance the speed?

We realized that there was no need to load all the pages of the PDF at once, as typically, only two pages are displayed on a mobile screen at any given time. So, preloading all the pages for the user didn't make sense.

The challenge then was to create our own custom pagination logic. To do this, we leveraged Android's Paging Library and developed a custom datasource class.

Here's the custom datasource class we created to implement our unique pagination logic.

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)
}
}

//It is the function that converts the image into bitmap
//and returns the uri string of it.
//This function involves complex processing of images
//So it is time taking function.
//This functions take the page and using pdfRender,
//we get the stream of data of particular page of pdf,
//which helps in converting it into the image.
private fun getBitmap(page: Int): String? {}
}

Quantitative Analysis

To put into numbers how much we managed to speed up PDF rendering, let's assume the time taken by the getBitmap(page: Page) function (which converts a PDF page into an image) is T. This function is the most time-consuming, and its duration determines the overall rendering time of the PDF. Let's say your PDF has P pages.

Previously, the PDF loading time was PT, as all pages were loaded at once. With pagination, it now takes only 2T, as just two pages are loaded at a time. This means we've reduced the time by (P-2)*T.

In percentage terms, the reduction in PDF rendering time is:

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

where:

P = number of pages in the PDF T = time taken by the getBitmap(page) function

After some debugging, we discovered that the getBitmap() function typically takes 5 seconds to run. So, T = 5 seconds.

For a PDF with P = 10 pages, the previous approach would have taken 50 seconds (P*T), while our new "lazy loading" approach takes just 10 seconds.

As you can see, we've managed to cut down the loading time by a whopping 80%.

And that, my friends, is the power of pagination!

To learn more about pagination you can visit the official guide

Authors
Vibhanshu Sharma
Share

Related Posts