> For the complete documentation index, see [llms.txt](https://docs.apryse.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.apryse.com/ios/get-started/frameworks/flutter/add-an-api.md).

# Add an API for Flutter

Learn how to configure new viewer options in Objective C for iOS development. Define, set, and use properties efficiently with step-by-step instructions. The Apryse iOS SDK streamlines secure document

### Introduction

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:

* Present a document via a plugin.
* Show an Apryse document view via a Widget.

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](https://flutter.dev/docs/development/platform-integration/platform-channels?tab=android-channel-java-tab) to have a better understanding of the system.

### 1. Fork and clone Apryse's Flutter Repo

The source is hosted on GitHub here: [https://github.com/ApryseSDK/pdftron-flutter](https://github.com/ApryseSDK/pdftron-flutter/)

Fork the project and clone a copy of the repository to your disk.

## Adding the `readOnly` viewer configuration option

### 2. Define the new viewer configuration option in Dart

The `config.dart` file is responsible for setting all of the viewer configuration options.

{% tabs %}
{% tab title="Dart" %}
{% code lineNumbers="true" %}

```dart
var _readOnly;

set readOnly(bool value) => _readOnly = value;

Config.fromJson(Map<String, dynamic> json) :
    _readOnly = json['readOnly'];

Map<String, dynamic> toJson() => {
    'readOnly': _readOnly,
};
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 3. Define the new viewer configuration option in Objective C

First add the property to `ios/Classes/PTFlutterDocumentController.h`:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
@property (nonatomic, assign, getter=isReadOnly) BOOL readOnly;
```

{% endcode %}
{% endtab %}
{% endtabs %}

Define a config constant for the property in `ios/Classes/PdftronFlutterPlugin.h`:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
// config
static NSString * const PTReadOnlyKey = @"readOnly";
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 4. Set the property

In `ios/Classes/PdftronFlutterPlugin.m`, we iterate through the config and check for the presence of keys. Add the config constant that you defined in the previous step, and set the property:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
+ (void)configureDocumentController:(PTFlutterDocumentController*)documentController withConfig:(NSString*)config
{
    ...            
    for (NSString* key in configPairs.allKeys) {
        if([key isEqualToString:PTDisabledToolsKey])
        {
            ...
        } else if ([key isEqualToString:PTReadOnlyKey]) {
            NSNumber* readOnlyNumber = [PdftronFlutterPlugin getConfigValue:configPairs configKey:PTReadOnlyKey class:[NSNumber class] error:&error];        
            if (!error && readOnlyNumber) {
                [documentController setReadOnly:[readOnlyNumber boolValue]];
            }
        }
```

{% endcode %}
{% endtab %}
{% endtabs %}

In this case, we call the default setter. Sometimes a custom setter must be defined in `ios/Classes/PTFlutterDocumentController.m`.

### 5. Use the property

In `ios/Classes/PTFlutterDocumentController.m`, give the property an initial value:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
- (void)initViewerSettings
{
    _base64 = NO;
    _readOnly = NO;
    ...
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

In the same file, use the property to set the `PTToolManager.readonly` property.

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
- (void)viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];

    if (self.needsDocumentLoaded) {
        ...
    }
    
    if (![self.toolManager isReadonly] && self.readOnly) {
        self.toolManager.readonly = YES;
    }
    
    [self.plugin.tabbedDocumentViewController.tabManager saveItems];
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

Now the document associated with the `PTPDFViewCtrl` is read-only.

## Adding the \`getPageCropBox\` function

### 2. Define the new function in Dart

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:

{% tabs %}
{% tab title="Dart" %}
{% code lineNumbers="true" %}

```dart
class Functions {
    static const getPageCropBox = "getPageCropBox";
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

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:

{% tabs %}
{% tab title="Dart" %}
{% code lineNumbers="true" %}

```dart
Future<PTRect> getPageCropBox(int pageNumber) {
    return _channel.invokeMethod(Functions.getPageCropBox, <String, dynamic>{'pageNumber': pageNumber}).then((value) => jsonDecode(value));
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

In `pdftron_flutter.dart`, add the `getPageCropBox` method to the `PdftronFlutter` class:

{% tabs %}
{% tab title="Dart" %}
{% code lineNumbers="true" %}

```dart
static Future<PTRect> getPageCropBox(int pageNumber) {
    return _channel.invokeMethod(Functions.getPageCropBox, <String, dynamic>{'pageNumber': pageNumber}).then((value) => jsonDecode(value));
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

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.

{% tabs %}
{% tab title="Dart" %}
{% code lineNumbers="true" %}

```dart
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;
    }
  }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 3. Receive the new function from method channel

First, define the function and argument constants in `ios/Classes/PdftronFlutterPlugin.h`:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
// function
static NSString * const PTGetPageCropBoxKey = @"getPageCropBox";
...

// argument
static NSString * const PTPageNumberArgumentKey = @"pageNumber";
...
```

{% endcode %}
{% endtab %}
{% endtabs %}

Sometimes the argument constants may already exist, so check before you define any constants.

Open the `ios/Classes/PdftronFlutterPlugin.m` file and add a new case for the new function to `handleMethodCall`:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
    if ([call.method isEqualToString:PTGetPlatformVersionKey]) {
        ...
    } else if ([call.method isEqualToString:PTGetPageCropBoxKey]) {
        NSNumber *pageNumber = [PluginUtils PT_idAsNSNumber:call.arguments[PTPageNumberArgumentKey]];
        [self getPageCropBox:pageNumber resultToken:result];
    } else {
        ...
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 4. Implement the new function

In the same file `ios/Classes/PdftronFlutterPlugin.m`, implement the new function:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
- (void)getPageCropBox:(NSNumber *)pageNumber resultToken:(FlutterResult)result
{
    PTDocumentViewController *docVC = [self getDocumentViewController];

    if(documentController.document == Nil)
    {
        // something is wrong, no document.
        NSLog(@"Error: The document view controller has no document.");
        flutterResult([FlutterError errorWithCode:@"get_page_crop_box" message:@"Failed to get page crop box" details:@"Error: The document view controller has no document."]);
        return;
    }

    NSError *error;
    [docVC.pdfViewCtrl DocLock:YES withBlock:^(PTPDFDoc * _Nullable doc) {
        if([doc HasDownloader])
        {
            // too soon
            NSLog(@"Error: The document is still being downloaded.");
            return;
        }
        
        PTPage *page = [doc GetPage:(int)pageNumber];
        if (page) {
            PTPDFRect *rect = [page GetCropBox];
            NSDictionary<NSString *, NSNumber *> *map = @{
                PTX1Key: @([rect GetX1]),
                PTY1Key: @([rect GetY1]),
                PTX2Key: @([rect GetX2]),
                PTY2Key: @([rect GetY2]),
                PTWidthKey: @([rect Width]),
                PTHeightKey: @([rect Height]),
            };
            NSData *jsonData = [NSJSONSerialization dataWithJSONObject:map options:0 error:nil];
            NSString *res = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
            
            result(res);
        }

    } error:&error];
    
    if(error)
    {
        NSLog(@"Error: There was an error while trying to get the page crop box. %@", error.localizedDescription);
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

The logic is to first get the current document view controller, lock the file and check whether the document is still being downloaded. If not, 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.

## Adding `startZoomChangedListener`

### 2. Define the new listener in Dart

The `events.dart` file is where all event listeners are defined and implemented. In this file do the following:

* add a constant to represent the event channel.
* declare a type alias for the listener.
* add an id to the enum `eventSinkId`.
* add a method that sets up a stream for our event.

{% tabs %}
{% tab title="Dart" %}
{% code lineNumbers="true" %}

```dart
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();
  };
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 3. Define necessary constants and functions on iOS

In `ios/Classes/PdftronFlutterPlugin.h` do the following:

* add an event key constant.
* add an id to the enum `eventSinkId`.
* add a method to the existing `PdftronFlutterPlugin` interface.

By the end of the guide, this method will send events to Dart and will be called whenever zoom changes.

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
// event strings
static NSString * const PTZoomChangedEventKey = @"zoom_changed_event";

typedef enum {
    ...
    zoomChangedId,
} EventSinkId;

@interface PdftronFlutterPlugin : NSObject<FlutterPlugin, FlutterStreamHandler, FlutterPlatformView>

-(void)documentController:(PTDocumentController *)docVC zoomChanged:(NSNumber*)zoom;

@end
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 4. Initialise event channel and sink

In order to send events to Flutter, we need an event channel and sink.

Add a new event sink property to `ios/Classes/PdftronFlutterPlugin.m`:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
@interface PdftronFlutterPlugin () <PTTabbedDocumentViewControllerDelegate, PTDocumentControllerDelegate>
...
@property (nonatomic, strong) FlutterEventSink zoomChangedEventSink;
...
@end
```

{% endcode %}
{% endtab %}
{% endtabs %}

In the same file, declare and initialize the event channel:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
- (void)registerEventChannels:(NSObject<FlutterBinaryMessenger> *)messenger
{
    ...
    FlutterEventChannel* zoomChangedEventChannel = [FlutterEventChannel eventChannelWithName:PTZoomChangedEventKey binaryMessenger:messenger];
    ...
    [zoomChangedEventChannel setStreamHandler:self];
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

The two methods below, `onListenWithArguments:eventSink:` and `onCancelWithArguments:eventSink:`, are used to start and cancel the event listener respectively.

In the same file, add a case for our event id to each method:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink)events
{
    
    int sinkId = [arguments intValue];
    
    switch (sinkId)
    {
        ...
        case zoomChangedId:
            self.zoomChangedEventSink = events;
            break;
    }

    return Nil;
}

- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments
{
    int sinkId = [arguments intValue];
    
    switch (sinkId)
    {
         ...
        case zoomChangedId:
            self.zoomChangedEventSink = nil;
            break;
    }

    return Nil;
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 5. Send event to Dart

In the same file, `ios/Classes/PdftronFlutterPlugin.m`, implement the method to send the event to Dart, using the new event sink:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
#pragma mark - EventSinks
-(void)documentController:(PTDocumentController *)docVC zoomChanged:(NSNumber*)zoom
{
    if (self.zoomChangedEventSink != nil)
    {
        self.zoomChangedEventSink(zoom);
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

In the next step, we will call this method whenever the event occurs.

### 6. Receive event from PDFViewCtrl

The `PTPDFViewCtrlDelegate` protocol contains the method `pdfViewCtrl:pdfScrollViewDidZoom:`. This method allows adopting delegates to respond to the `PTPDFViewCtrl` class when the zoom event occurs.

Implement this method in `ios/Classes/PTFlutterDocumentController.m`:

{% tabs %}
{% tab title="Obj-C" %}
{% code lineNumbers="true" %}

```objc
- (void)pdfViewCtrl:(PTPDFViewCtrl*)pdfViewCtrl pdfScrollViewDidZoom:(UIScrollView *)scrollView
{
    const double zoom = self.pdfViewCtrl.zoom * self.pdfViewCtrl.zoomScale;
    [self.plugin documentController:self zoomChanged:[NSNumber numberWithDouble:zoom]];
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

The actual implementation will depend on the actual functionality.

## Finishing steps

### 1. Push the code and integrate the updated source

Now update your library with the new code.

The new functionality is now ready to use.

### 2. Access the new functionality

The app can now access the new APIs as follows:

Plugin version:

{% tabs %}
{% tab title="Dart" %}
{% code lineNumbers="true" %}

```dart
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
```

{% endcode %}
{% endtab %}
{% endtabs %}

Or:

Widget version:

{% tabs %}
{% tab title="Dart" %}
{% code lineNumbers="true" %}

```dart
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
```

{% endcode %}
{% endtab %}
{% endtabs %}

### 3. All done!

If you're only developing for iOS, then you're all done!

If you're also deploying on Android, you'll need to complete the necessary steps for Android.

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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.apryse.com/ios/get-started/frameworks/flutter/add-an-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
