Generating PDF reports on Android using HTML and WebView in the background

Introduction to the problem

The service, first of all, must solve the user’s problem. Often applications must do things that would seem to fall on the shoulders of the server and other public APIs. This, for example, happened in our project. The service helps create reports, manage them conveniently, and automates tasks of generating and sending them to clients. As part of this article, I want to talk about our experience of transferring the functionality of generating PDF reports to an Android device. Enjoy reading!

How it was…

Service operation up to

Service operation before changes

We solved the case in the following way: the client fills out the form in the application (additionally receiving the current form from the server in advance); sends JSON and MediaPart photos to the server; the server generates a PDF report and sends it to the client/shows it in the admin panel.

What's wrong?

At employee places of work there is often no Internet (or any connection at all), reports contain a large number of photographs, the server may not work correctly, and therefore the employee wants to have the report on his phone, regardless of the operation of the rest of the infrastructure.

The customers themselves constantly offered their “brilliant ideas” to improve the service, which we dutifully developed. The application even acquired a server selector where to send the report, and the corresponding several server instances, but that’s a completely different story… We solved the problem described above by transferring the task of generating reports directly to the client application and transferring a ready-made PDF to the server.

The new scheme of the project looks like this:

Service operation after changes

Service operation after changes

Implementation

Below we will go through the implementation of such a solution. I'll tell you how we did it, what we used and what alternatives there are.

What to use to generate PDF?

Alternatives

For those who want to consider other options, here are probably the most basic, well-known tools.

PDFDocument

PDFDocument – a basic tool for working with PDF included in android.graphics. Without a doubt, it is convenient for fast, reliable PDF editing and creating simple documents. In our case, it is irrelevant, since low-level capabilities are provided, it is difficult to work with large files, and even more so to quickly change their format.

PDFBox

PDFBox – a tool from Apache. In theory, they won’t do shit, but in our case it’s too much. Convenient work with tables and text. I don’t really like working with adding pictures, of which there can be a lot in a report (over 200).

iText

iText – newer than previous solutions, with a good community. We didn't choose it for the same reasons as PDFBox. Overall, it’s a pretty convenient, complete library; in other cases I would start with it.

Our choice

But our choice fell on a more crutch (possibly), but the simplest and fastest solution to implement. Namely, we create an HTML document using pieces of templates, run them through WebView for printing and save the resulting PDF.

Why is this so?

  1. Not from scratch. We already had a similar mechanism established on the server (read introduction). Accordingly, there were already HTML and CSS templates.

  2. Convenience and flexibility. HTML provides an easier and more flexible way to structure and style content. It adapts itself for printing on different paper formats. If your company's design changes, you can easily adjust the CSS.

  3. Working with images. In HTML it’s simply more convenient to work with images: easy to insert, familiar stylization, adaptive. It’s convenient to leave the URI before the image; all the rest of the work is taken care of WebView.

  4. Fast start. The project was already in production, but the functionality was urgently needed. Working with someone you previously knew WebView and HTML will reduce labor hours.

  5. Preview. You can conveniently organize a preview directly from the application. In the future, we abandoned this, since we had previously implemented a short summary for previewing the report, which is quite enough.

In general, the combination of these factors influenced the choice of technology.

Certain examples are provided as code. The article does not aim to show the correct patterns. For the tools described, it is better to make interfaces, break down the logic, etc.❗️

Let's start with preparing HTML

Let's implement PdfGeneratorwhich will take Context to work with WebViewconservation and resources.

The class itself must have a public function for creating and saving a document, where we pass data and a file to save. This function returns the result of operations.

class PdfGenerator(private val context: Context) {
  private val htmlToPdfConvertor = HtmlToPdfConvertor(context)
  // Функция генерации и сохранения
  suspend fun generateExpertAct(
      act: ActSave,
      images: List<File>,
      outFile: File
  ): Result<Unit> { <..> }
  
  // Функция генерации html контента
  private fun generateExpertActHtml(
      act: ActSave,
      images: List<File>
  ): String { <..> }
  
  <..>
  
  companion object {
      const val TAG: String = "PdfGenerator"
  
      private val PREFIX = """
            <!DOCTYPE html>
            <html lang="ru">
            <head>
                <meta charset="UTF-8">
                <title>Title</title>
            </head>
            
        """.trimIndent()
  
      private val SUFFIX = """
            </html>
            
        """.trimIndent()
        
      <..>
  }
}

Example code on Gist

Generating PDF

The code above is creating an instance HtmlToPdfConvertorwhich will do the work with WebView and conservation. It is written using callbacks, so you need to turn it into a suspend function using suspendCoroutine. Example below:

suspend fun generateExpertAct(
  act: ActSave,
  images: List<File>,
  outFile: File
): Result<Unit> {
  return suspendCoroutine { continuation ->
    Handler(Looper.getMainLooper()).post {
      htmlToPdfConvertor.convert(
        pdfLocation = outFile,
        htmlString = generateExpertActHtml(
          act = act,
          images = images
        ),
        onPdfGenerationFailed = { exception ->
          continuation.resume(Result.failure(exception))
        },
        onPdfGenerated = {
          continuation.resume(Result.success(Unit))
        }
      )
    }
  }
}

Example code on Gist

Wrapping everything in Worker

It has become important for the project that the sending and generating tasks are executed and run independently, for this it is worth doing Worker.

In the example code, working with Repository supplemented with comments to make it clear what is happening. You can learn more about background work read in the documentation.

Example code on Gist

WebView operation

In case you do not display WebView (as a preview) and want to trigger PDF generation in the background (Worker), then it is necessary to “create conditions for this”.

Work with WebView executed in a block Handler(Looper.getMainLooper()).post, which is used to execute code on the application's main thread. In Android, the main thread is also called the UI thread.

Result

This problem was solved using a non-standard method. The solution cannot be called recommended; it is better to use specialized libraries. But if you need to use exactly this method, then perhaps my experience will be useful.

In our project, this approach works stably and quite quickly. Reports include over 100 photos, all added without errors. This solution allowed us to quickly change the appearance of documents to suit the new style of the company, as well as add Plotly infographics by connecting JS.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *