Some test text!
Android / Guides / Add an API
The Flutter API for Apryse Mobile SDK includes all of the most used functions and methods for viewing, annotating and saving PDF documents. However, it is possible your app may need access to APIs that are available as part of the native API, but are not directly available to Flutter.
There are 2 different ways to use Apryse Flutter API on iOS or Android:
This guide will demonstrate how to add a new functionality to both approaches.
The examples provided will show you how to add the following to the Flutter interface:
readOnly
viewer configuration option that determines if the viewer will allow edits to the document.getPageCropBox
function that returns a PTRect
object containing information about the crop box of a specified page.startZoomChangedListener
event listener that is raised when the zoom ratio is changed in the current document.You can follow the same pattern to add new functions and viewer configuration options that your Flutter app may need. These additions could be simple ones, which expose one piece of functionality, or custom ones, that expose a series of native commands under the hood.
Prior to following this guide, we highly recommend you to go through the official guide here: Writing Platform-specific code to have a better understanding of the system.
The source is hosted on GitHub here: https://github.com/ApryseSDK/pdftron-flutter
Fork the project and clone a copy of the repository to your disk.
readOnly
viewer configuration optionThe config.dart
file is responsible for setting all of the viewer configuration options.
var _readOnly;
set readOnly(bool value) => _readOnly = value;
Config.fromJson(Map<String, dynamic> json) :
_readOnly = json['readOnly'];
Map<String, dynamic> toJson() => {
'readOnly': _readOnly,
};
First define a config constant for the property in /android/src/main/java/com/pdftron/pdftronflutter/helpers/PluginUtils.java
:
public static final String KEY_CONFIG_READ_ONLY = "readOnly";
In the same file, /android/src/main/java/com/pdftron/pdftronflutter/helpers/PluginUtils.java
, add the code to set the property:
public static ConfigInfo handleOpenDocument(@NonNull ViewerConfig.Builder builder, @NonNull ToolManagerBuilder toolManagerBuilder,
@NonNull PDFViewCtrlConfig pdfViewCtrlConfig, @NonNull String document, @NonNull Context context,
String configStr) {
...
if (configStr != null && !configStr.equals("null")) {
try {
if (!configJson.isNull(KEY_CONFIG_READ_ONLY)) {
boolean readOnly = configJson.getBoolean(KEY_CONFIG_READ_ONLY);
builder.documentEditingEnabled(!readOnly);
}
}
}
}
## Adding the `getPageCropBox` function
The constants.dart
file contains constants grouped by their purpose. The names of functions, parameters, buttons, toolbars, and other relevant constants are stored here. Add a constant to the Functions
class:
class Functions {
static const getPageCropBox = "getPageCropBox";
}
The document_view.dart
file handles function calls for the widget, while pdftron_flutter.dart
handles function calls for the plugin. If you want to implement for both the widget and plugin then add functions to both files, otherwise add your function to whichever you prefer.
Note that since we use JSON to transfer the information between Flutter and the native code, one extra step of decoding is required here.
In document_view.dart
, add the getPageCropBox
method to the DocumentViewController
class:
Future<PTRect> getPageCropBox(int pageNumber) {
return _channel.invokeMethod(Functions.getPageCropBox, <String, dynamic>{'pageNumber': pageNumber}).then((value) => jsonDecode(value));
}
In pdftron_flutter.dart
, add the getPageCropBox
method to the PdftronFlutter
class:
static Future<PTRect> getPageCropBox(int pageNumber) {
return _channel.invokeMethod(Functions.getPageCropBox, <String, dynamic>{'pageNumber': pageNumber}).then((value) => jsonDecode(value));
}
The options.dart
file contains constants and classes for convenience. In this exercise, we add a new class called PTRect
to this file for easier access of information.
class PTRect {
double x1, y1, x2, y2, width, height;
PTRect(this.x1, this.y1, this.x2, this.y2, this.width, this.height);
factory PTRect.fromJson(dynamic json) {
return PTRect(getDouble(json['x1']), getDouble(json['y1']), getDouble(json['x2']), getDouble(json['y2']), getDouble(json['width']), getDouble(json['height']));
}
// a helper for JSON number decoding
static getDouble(dynamic value) {
if (value is int) {
return value.toDouble();
} else {
return value;
}
}
}
First, define the function and argument constants in /android/src/main/java/com/pdftron/pdftronflutter/helpers/PluginUtils.java
, (constants are grouped according to purpose):
public static final String KEY_PAGE_NUMBER = "pageNumber";
...
public static final String FUNCTION_GET_PAGE_CROP_BOX = "getPageCropBox";
Then, you have 3 options:
You would like the new function implemented for the plugin version.
Open the /android/src/main/java/com/pdftron/pdftronflutter/PdftronFlutterPlugin.java
file and add a new case for the new function to onMethodCall
:
@Override
public void onMethodCall(MethodCall call, Result result) {
switch (call.method) {
case FUNCTION_GET_PAGE_CROP_BOX: {
FlutterDocumentActivity flutterDocumentActivity = FlutterDocumentActivity.getCurrentActivity();
Objects.requireNonNull(flutterDocumentActivity);
Objects.requireNonNull(flutterDocumentActivity.getPdfDoc());
Integer pageNumber = call.argument(KEY_PAGE_NUMBER);
if (pageNumber != null) {
try {
flutterDocumentActivity.getPageCropBox(pageNumber, result);
} catch (JSONException ex) {
ex.printStackTrace();
result.error(Integer.toString(ex.hashCode()), "JSONException Error: " + ex, null);
} catch (PDFNetException ex) {
ex.printStackTrace();
result.error(Long.toString(ex.getErrorCode()), "PDFTronException Error: " + ex, null);
}
}
break;
}
}
}
You would like the new function implemented for the widget version.
Open the /android/src/main/java/com/pdftron/pdftronflutter/FlutterDocumentView.java
file and add a new case for the new function to onMethodCall
:
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
switch (call.method) {
case FUNCTION_GET_PAGE_CROP_BOX: {
Objects.requireNonNull(documentView);
Objects.requireNonNull(documentView.getPdfDoc());
Integer pageNumber = call.argument(KEY_PAGE_NUMBER);
if (pageNumber != null) {
try {
documentView.getPageCropBox(pageNumber, result);
} catch (JSONException ex) {
ex.printStackTrace();
result.error(Integer.toString(ex.hashCode()), "JSONException Error: " + ex, null);
} catch (PDFNetException ex) {
ex.printStackTrace();
result.error(Long.toString(ex.getErrorCode()), "PDFTronException Error: " + ex, null);
}
}
break;
}
}
}
You would like the new function implemented for both versions. Note that this option is only for convenience and easier maintenance; you could always just use Options 1 and/or 2.
Open the /android/src/main/java/com/pdftron/pdftronflutter/helpers/PluginUtils.java
file and add a new case for the new function to onMethodCall
:
public static void onMethodCall(MethodCall call, MethodChannel.Result result, ViewActivityComponent component) {
switch (call.method) {
case FUNCTION_GET_PAGE_CROP_BOX: {
checkFunctionPrecondition(component);
Integer pageNumber = call.argument(KEY_PAGE_NUMBER);
if (pageNumber != null) {
try {
getPageCropBox(pageNumber, result, component);
} catch (JSONException ex) {
ex.printStackTrace();
result.error(Integer.toString(ex.hashCode()), "JSONException Error: " + ex, null);
} catch (PDFNetException ex) {
ex.printStackTrace();
result.error(Long.toString(ex.getErrorCode()), "PDFTronException Error: " + ex, null);
}
}
break;
}
}
}
Following step 3, 3 options will be listed below.
You would like the new function implemented for the plugin version.
Open the /android/src/main/java/com/pdftron/pdftronflutter/FlutterDocumentActivity.java
file and implement the new function:
public void getPageCropBox(int pageNumber, Result result) throws PDFNetException, JSONException {
JSONObject jsonObject = new JSONObject();
PDFDoc pdfDoc = getPdfDoc();
if (pdfDoc == null) {
result.error("InvalidState", "Activity not attached", null);
return;
}
Rect rect = pdfDoc.getPage(pageNumber).getCropBox();
jsonObject.put(KEY_X1, rect.getX1());
jsonObject.put(KEY_Y1, rect.getY1());
jsonObject.put(KEY_X2, rect.getX2());
jsonObject.put(KEY_Y2, rect.getY2());
jsonObject.put(KEY_WIDTH, rect.getWidth());
jsonObject.put(KEY_HEIGHT, rect.getHeight());
result.success(jsonObject.toString());
}
You would like the new function implemented for the widget version.
Open the /android/src/main/java/com/pdftron/pdftronflutter/views/DocumentView.java
file and implement the new function:
public void getPageCropBox(int pageNumber, MethodChannel.Result result) throws PDFNetException, JSONException {
JSONObject jsonObject = new JSONObject();
PDFDoc pdfDoc = getPdfDoc();
if (pdfDoc == null) {
result.error("InvalidState", "Activity not attached", null);
return;
}
Rect rect = pdfDoc.getPage(pageNumber).getCropBox();
jsonObject.put(KEY_X1, rect.getX1());
jsonObject.put(KEY_Y1, rect.getY1());
jsonObject.put(KEY_X2, rect.getX2());
jsonObject.put(KEY_Y2, rect.getY2());
jsonObject.put(KEY_WIDTH, rect.getWidth());
jsonObject.put(KEY_HEIGHT, rect.getHeight());
result.success(jsonObject.toString());
}
You would like the new function implemented for both versions.
In the same file /android/src/main/java/com/pdftron/pdftronflutter/helpers/PluginUtils.java
, implement the new function:
private static void getPageCropBox(int pageNumber, MethodChannel.Result result, ViewActivityComponent component) throws PDFNetException, JSONException {
JSONObject jsonObject = new JSONObject();
PDFDoc pdfDoc = component.getPdfDoc();
if (pdfDoc == null) {
result.error("InvalidState", "Activity not attached", null);
return;
}
Rect rect = pdfDoc.getPage(pageNumber).getCropBox();
jsonObject.put(KEY_X1, rect.getX1());
jsonObject.put(KEY_Y1, rect.getY1());
jsonObject.put(KEY_X2, rect.getX2());
jsonObject.put(KEY_Y2, rect.getY2());
jsonObject.put(KEY_WIDTH, rect.getWidth());
jsonObject.put(KEY_HEIGHT, rect.getHeight());
result.success(jsonObject.toString());
}
The logic is to first get the current doc, get the crop box and encode all the associated information into a JSON string as the result.
The actual implementation will depend on the actual functionality.
startZoomChangedListener
The events.dart
file is where all event listeners are defined and implemented. In this file do the following:
eventSinkId
.const _zoomChangedChannel = const EventChannel('zoom_changed_event');
typedef void ZoomChangedListener(dynamic zoom);
enum eventSinkId {
zoomChangedId,
}
CancelListener startZoomChangedListener(ZoomChangedListener listener) {
var subscription = _zoomChangedChannel
.receiveBroadcastStream(eventSinkId.zoomChangedId.index)
.listen(listener, cancelOnError: true);
return () {
subscription.cancel();
};
}
The ViewerComponent
interface contains various methods that can be overridden by subclasses. In /android/src/main/java/com/pdftron/pdftronflutter/helpers/ViewerComponent.java
, add a getter for an event emitter:
EventChannel.EventSink getZoomChangedEventEmitter();
The FlutterDocumentActivity
class extends ViewerComponent
and is used for the plugin version of our APIs. Do the following steps in /android/src/main/java/com/pdftron/pdftronflutter/FlutterDocumentActivity.java
:
private static AtomicReference<EventSink> sZoomChangedEventEmitter = new AtomicReference<>();
public static void setZoomChangedEventEmitter(EventSink emitter) {
sZoomChangedEventEmitter.set(emitter);
}
@Override
public EventSink getZoomChangedEventEmitter() {
return sZoomChangedEventEmitter.get();
}
@Override
protected void onDestroy() {
PluginUtils.handleOnDetach(this);
super.onDestroy();
...
sZoomChangedEventEmitter.set(null);
detachActivity();
}
The DocumentView
class extends ViewerComponent
and is used for the widget version of our APIs. Do the following steps in /android/src/main/java/com/pdftron/pdftronflutter/views/DocumentView.java
:
private EventChannel.EventSink sZoomChangedEventEmitter;
public void setZoomChangedEventEmitter(EventChannel.EventSink emitter) {
sZoomChangedEventEmitter = emitter;
}
@Override
public EventChannel.EventSink getZoomChangedEventEmitter() {
return sZoomChangedEventEmitter;
}
In /android/src/main/java/com/pdftron/pdftronflutter/helpers/PluginUtils.java
, add a constant for the event.
public static final String EVENT_ZOOM_CHANGED = "zoom_changed_event";
For the plugin version, set up the event channel and stream handler in /android/src/main/java/com/pdftron/pdftronflutter/PdftronFlutterPlugin.java
. In the stream handler, set our event emitter when an event stream is created, and remove it when an event stream is torn down.
import static com.pdftron.pdftronflutter.helpers.PluginUtils.EVENT_ZOOM_CHANGED;
...
public static void registerWith(Registrar registrar) {
final MethodChannel methodChannel = new MethodChannel(registrar.messenger(), "pdftron_flutter");
methodChannel.setMethodCallHandler(new PdftronFlutterPlugin(registrar.activeContext()));
...
final EventChannel zoomChangedEventChannel = new EventChannel(registrar.messenger(), EVENT_ZOOM_CHANGED);
zoomChangedEventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object arguments, EventChannel.EventSink emitter) {
FlutterDocumentActivity.setZoomChangedEventEmitter(emitter);
}
@Override
public void onCancel(Object arguments) {
FlutterDocumentActivity.setZoomChangedEventEmitter(null);
}
});
registrar.platformViewRegistry().registerViewFactory("pdftron_flutter/documentview", new DocumentViewFactory(registrar.messenger(), registrar.activeContext()));
}
For the widget version, set up the event channel and stream handler in /android/src/main/java/com/pdftron/pdftronflutter/FlutterDocumentView.java
. In the stream handler, set our event emitter when an event stream is created, and remove it when an event stream is torn down.
import static com.pdftron.pdftronflutter.helpers.PluginUtils.EVENT_ZOOM_CHANGED;
...
public void registerWith(BinaryMessenger messenger) {
...
final EventChannel zoomChangedEventChannel = new EventChannel(messenger, EVENT_ZOOM_CHANGED);
zoomChangedEventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object arguments, EventChannel.EventSink emitter) {
documentView.setZoomChangedEventEmitter(emitter);
}
@Override
public void onCancel(Object arguments) {
documentView.setZoomChangedEventEmitter(null);
}
});
}
In /android/src/main/java/com/pdftron/pdftronflutter/helpers/ViewerImpl.java
, set up an event listener that sends the event to Flutter. Note that the necessary listener may already exist.
private PDFViewCtrl.OnCanvasSizeChangeListener mOnCanvasSizeChangedListener = new PDFViewCtrl.OnCanvasSizeChangeListener() {
@Override
public void onCanvasSizeChanged() {
EventChannel.EventSink eventSink = mViewerComponent.getZoomChangedEventEmitter();
if (eventSink != null && mViewerComponent.getPdfViewCtrl() != null) {
eventSink.success(mViewerComponent.getPdfViewCtrl().getZoom());
}
}
};
The actual implementation will depend on the actual functionality.
Now update your library with the new code.
The new functionality is now ready to use.
The app can now access the new APIs as follows:
Plugin version:
var config = Config();
config.readOnly = true;
await PdftronFlutter.openDocument("https://pdftron.s3.amazonaws.com/downloads/pl/PDFTRON_mobile_about.pdf", config: config);
var cropBox = await PdftronFlutter.getPageCropBox(1);
print('The width of crop box for page 1 is: ' + cropBox.width.toString());
var zoomChangedCancel = startZoomChangedListener((zoom) {
print("flutter zoom changed. Current zoom is: $zoom");
});
zoomChangedCancel(); // Cancels the listener
Or:
Widget version:
var config = Config();
config.readOnly = true;
await controller.openDocument("https://pdftron.s3.amazonaws.com/downloads/pl/PDFTRON_mobile_about.pdf", config: config);
var cropBox = await controller.getPageCropBox(1);
print('The width of crop box for page 1 is: ' + cropBox.width.toString());
var zoomChangedCancel = startZoomChangedListener((zoom) {
print("flutter zoom changed. Current zoom is: $zoom");
});
zoomChangedCancel(); // Cancels the listener
If you're only developing for Android, then you're all done!
If you're also deploying on iOS, you'll need to complete the necessary steps for iOS.
If you're developing for both iOS and Android, please consider submitting a PR, as upstreaming the change will simplify your developing and make the APIs available for other Apryse customers.
Trial setup questions? Ask experts on Discord
Need other help? Contact Support
Pricing or product questions? Contact Sales