I ran into an issue some time ago. I wanted to build Flutter application that could convert PDF files to speech, to help me with school work. The idea was simple; read the text from PDF file and convert it to speech with the flutter_tts plugin. However, I ran into a problem, the PDF plugins for Flutter don’t extract the text from the file they just open native view and show the file as PDF. So to get around this issue I decided to build my own PDF text extractor plugin.
I will split this tutorial to two parts, first part will cover the Android side and second will cover iOS. I will also go over how package plugins work and how they are structured.
- I expect you to have a basic understanding of Flutter.
- You will need to have Flutter installed and configured.
- I recommend writing native code on Android Studio so that you know it works before adding it to the plugin. This is what I did, but it is not showcased in this tutorial.
How do Flutter packages work?
Flutter defines two types of packages; dart packages and plugin packages. Dart packages are plain dart code that you can add to your application, plugin packages are packages that run code on the native side; Android and iOS. We are going to be writing a plugin package.
Flutter uses MethodChannels to talk to the native code. There is a host on the native side that listens for calls from the Flutter- side and responds to those calls. This guide is based on the official documentation by Flutter, I recommend checking it out.
Creating a plugin package
First run Flutter doctor to check that everything is working.
If needed upgrade Flutter to the latest version.
I am using the stable build of Flutter for this project. The stable build should have less bugs in it. We can check the build channel, and change the channel to stable using these commands.
flutter channelflutter channel stable
We can use the command below to create a new plugin package. The package includes an example project that shows the platform version of your device.
flutter create --template=plugin -i swift -a java pdf_text_package
We can use the -i and -a to set the languages that we want to use for our package. I chose Swift for iOS and Java for Android because I’m more familiar with those languages. If you want you can also use Objective-C or Kotlin.
After creating the package we can open the package with our favorite code editor (for me VS Code). You should see something like this (obviously with your package name). I use Material Icon Theme in VS Code, that’s why the icons might look different.
The most important directories here are: android, ios, example and lib directories.
Android and iOS directories
In the android directory we have an Android Java project but with some boilerplate code to connect with Flutter.
From the android directory we can find the PdfTextPackagePlugin.java (depending on your package name), this file hosts the Java code that we want to run from Flutter. In the Java file we have four methods and one variable.
- channel, this reference variable is used to hold a reference to the Flutter package, to register and detach from it.
- onAttachedToEngine, is called when the Android host is attached to the Flutter package.
- registerWith, similar to the onAttachedToEngine used for backwards compatibility.
- onMethodCall, this method is called when the Android host receives a method call from the Flutter package. This is where we will implement code.
- onDetachedFromEngine, a method that is called when the Flutter application is detaching from the Android host.
The ios directory hosts a Swift project, we are not going to be covering it in this article. But the idea is the same as with the android directory.
The lib directory hosts the Dart code for our plugin package.
In here we find more code generated by Flutter. The
platformVersiongetter is used to make a method call in the native code and
MethodChannel _channel is used to connect to the Android host.
In the example directory we have an example application which we can use to test the package. It is already configured to run our
PdfTextPackagefrom the lib directory. We can run the example application from the main.dart file.
In summary, here are the files we need to change in order to call code from the Android-side.
Running the example
Now that we understand the structure of the package we can run the example application that Flutter has generated for us. Navigate to the example directory inside the package with CMD and run the command below. You will need to have your device running.
Writing the package
We are going to start by writing the Java code. What I want to do is parse the text out of PDF documents. However, Android doesn’t support this functionality out of the box, so I need to add the PDFbox library to my Android project to parse the text.
Adding PDFbox to the Android project
We can do this by adding the following lines to your build.gradle file.
Import the PDFbox package and other libraries to the Java file.
In the PDFbox documentation they instruct us to do the following.
“Before calls to PDFBox are made it is highly recommended to initialize the library’s resource loader. Add the following line before calling PDFBox methods”
This means that we need to add the following lines to
Package Java code
Now we can add the following code to
onMethodCall method in the Java file.
- Basically what is happening here is that we are getting the method call from Dart using.
- Then we get the path of the PDF file from arguments of the call using.
final String path = call.argument("path")
- We create new File object and set that to the path and load that file.
File renderFile = new File(path);document = PDDocument.load(renderFile);
- We create PDF stripper and parse the text from the PDF document.
PDFTextStripper pdfStripper = new PDFTextStripper();parsedText = pdfStripper.getText(document);
- When we have the text we can close the document.
if (document != null) document.close();
- Lastly, we can return the text to the Dart side.
Package Dart code
After the Java code is done we can navigate to the Dart code of our package, and add the
getPDFtext static method. We can use key-value pairs to pass in the path of the PDF file.
Example app code
Navigate to the example directory. Add file_picker to the project’s pubspec.yaml file and fetch the plugin. We are using File Picker to get the path of our PDF file.
flutter pub get
Import file_picker and dart.io to the main.dart file.
import 'package:file_picker/file_picker.dart';import 'dart:io';
Next we are going to finish the example app, by rewriting the main.dart file. You can copy paste from here.
Here we have a method
getPDFtext(path) which is responsible for calling the native Android code. It is using the static method
PdfTextPackge.getPDFtext() that we defined earlier.
text = await PdfTextPackage.getPDFtext(path);
When clicking the
FlatButton we call
FilePicker.getFile() method that is used to get the path of our PDF file, file picker returns the path, and
getPDFtext method is called. When the native code has parsed the text from the PDF document, it is returned to Dart side and set into the _pdfText variable.
Now when we run the application, then select the PDF document, we should get back the text to the Text-widget.
This was an interesting experience, writing a plugin package. I learned a lot and I recommend package development for everybody. :)
It needs to be noted that this plugin is not suitable for real use, the PDFbox library will cause Flutter UI to freeze. To get around this we would have to implement threading in the Android code. If you would like to see how that could be done you can visit the GitHub of my read_pdf_text package, which is a more advanced version of the plugin that we developed in this article.
Please leave comments and questions if there is something unclear or you have some critique. I am always trying to improve! :D