Flutter is an open-source framework by Google for building native mobile apps. Flutter compiles Dart widgets into native components to provide users the fluid look and feel of traditional native apps. And its learn-once, write-anywhere codebase makes Flutter a popular choice to develop cross-platform. As Flutter runs on both native and the web, it helps with a faster time to market and UX consistency, but it retains flexibility for platform-specific logic.

Being fully cross-platform capable as well, the PDFTron SDK pairs nicely with Flutter. This post shows how to use Flutter and PDFTron to create a cross-platform PDF, MS Office, and image file viewer -- like the one embedded below!

Follow the steps below to create your own viewer for both native mobile platforms and the web, no servers required. Then we’ll show you how easy it is to use PDFTron to add more features, like annotations, signatures, redaction, and more.

Setup

First, follow the guide from the official Flutter site to ensure you have the web option enabled. Then run:

flutter create myapp
cd myapp

This creates an app that works on native and the web.

Integrate with PDFTron SDK

Next, we will add PDFTron's Flutter SDK as well as WebViewer into the app.

Native

Follow the instructions here to add PDFTron's Flutter module to the app. Then, follow step 2-5(a) for Android, and step 2-4 for iOS.

Android

Create a local.properties file inside the android folder with your Android SDK location, for example:

ndk.dir=/Users/<user-name>/Library/Android/sdk/ndk-bundle
sdk.dir=/Users/<user-name>/Library/Android/sdk

Note: replace with your actual path.

Web

  1. Download and unzip the WebViewer package, then place it in the root folder of this app.
  2. In file web/index.html, add a <script> tag inside the <head> tag:

    <head>
    ...
    <script src="Webviewer/lib/webviewer.min.js"></script>
    ...
    </head>

Create the PDF Viewer

Create a pdfviewer folder in the lib directory, then add the following files:

  • pdfviewer_interface.dart - the interface of the viewer
  • pdfviewer_web.dart - the web implementation of the viewer
  • pdfviewer_native.dart - the native implementation of the viewer
  • pdfviewer_unsupported.dart - the viewer for unsupported platforms

Viewer Interface

Add in pdfviewer_interface.dart:

import 'package:flutter/material.dart';

import 'pdfviewer_unsupported.dart'
    if (dart.library.io) 'pdfviewer_native.dart'
    if (dart.library.html) 'pdfviewer_web.dart';

abstract class PDFViewer extends Widget {
  factory PDFViewer(String document) => getPDFViewer(document);
}

Unsupported Viewer

Add in pdfviewer_unsupported.dart:

import 'pdfviewer_interface.dart';

PDFViewer getPDFViewer(String document) => throw UnsupportedError(
    'Cannot create a viewer when platform is neither web nor mobile');

Native Viewer

Add in pdfviewer_native.dart:

import 'dart:async';
import 'dart:io' show Platform;

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:myapp/pdfviewer/pdfviewer_interface.dart';
import 'package:pdftron_flutter/pdftron_flutter.dart';
import 'package:permission_handler/permission_handler.dart';

class NativeViewer extends StatefulWidget implements PDFViewer {
  final String _document;

  NativeViewer(this._document);

  @override
  _NativeViewerState createState() => _NativeViewerState();
}

class _NativeViewerState extends State<NativeViewer> {
  String _version = 'Unknown';

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    String version;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      PdftronFlutter.initialize("your_pdftron_license_key");
      version = await PdftronFlutter.version;
    } on PlatformException {
      version = 'Failed to get platform version.';
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _version = version;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
      width: double.infinity,
      height: double.infinity,
      child: DocumentView(
        onCreated: onDocumentViewCreated,
      ),
    ));
  }

  void onDocumentViewCreated(DocumentViewController controller) {
    if (Platform.isIOS) {
      showViewer(controller);
    } else {
      launchWithPermission(controller);
    }
  }

  Future<void> launchWithPermission(DocumentViewController controller) async {
    Map<PermissionGroup, PermissionStatus> permissions =
        await PermissionHandler().requestPermissions([PermissionGroup.storage]);
    if (granted(permissions[PermissionGroup.storage])) {
      showViewer(controller);
    }
  }

  bool granted(PermissionStatus status) {
    return status == PermissionStatus.granted;
  }

  void showViewer(DocumentViewController controller) {
    // shows how to disable functionality
    //  var disabledElements = [Buttons.shareButton, Buttons.searchButton];
    //  var disabledTools = [Tools.annotationCreateLine, Tools.annotationCreateRectangle];
    var config = Config();
    //  config.disabledElements = disabledElements;
    //  config.disabledTools = disabledTools;
    //  PdftronFlutter.openDocument(_document, config: config);

    // opening without a config file will have all functionality enabled.
    controller.openDocument(widget._document, config: config);
  }
}

PDFViewer getPDFViewer(String document) => NativeViewer(document);

WebViewer

Add in pdfviewer_web.dart:

import 'package:flutter/material.dart';

import 'dart:ui' as ui;
import 'dart:html' as html;

import 'package:myapp/pdfviewer/pdfviewer_interface.dart';

class WebViewer extends StatefulWidget implements PDFViewer {
  final String _document;

  WebViewer(this._document);

  // This widget is the home page of your application. It is stateful, meaning
  // that it has a State object (defined below) that contains fields that affect
  // how it looks.

  // This class is the configuration for the state. It holds the values (in this
  // case the title) provided by the parent (in this case the App widget) and
  // used by the build method of the State. Fields in a Widget subclass are
  // always marked "final".

  @override
  _WebViewerState createState() => _WebViewerState();
}

class _WebViewerState extends State<WebViewer> {
  String viewID = "webviewer-id";
  html.DivElement _element;

  @override
  void initState() {
    super.initState();

    _element = html.DivElement()
      ..id = 'canvas'
      ..append(html.ScriptElement()
        ..text = """
        const canvas = document.querySelector("flt-platform-view").shadowRoot.querySelector("#canvas");
        WebViewer({
          path: 'WebViewer/lib',
          initialDoc: '${widget._document}'
        }, canvas).then((instance) => {
            // call apis here
        });
        """);

    // ignore: undefined_prefixed_name
    ui.platformViewRegistry
        .registerViewFactory(viewID, (int viewId) => _element);
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
        appBar: AppBar(
          title: Text('PDFTron Flutter Demo'),
        ),
        body: FractionallySizedBox(
          widthFactor: 1,
          heightFactor: 1,
          child: Container(
            alignment: Alignment.center,
            child: HtmlElementView(
              viewType: viewID,
            ),
          ),
        ));
  }
}

PDFViewer getPDFViewer(String document) => WebViewer(document);

Use the PDF Viewer

In main.dart, this viewer component can now be used:

@override
Widget build(BuildContext context) {
  String document =
      "https://pdftron.s3.amazonaws.com/downloads/pl/PDFTRON_about.pdf";
  return MaterialApp(
    home: PDFViewer(document),
  );
}

The above code will pick up the native component on mobile platforms and the WebViewer component on the web. The rest of your application can share the same code, both for business logic and the UI.

That's it!

You can find full source code from here.

Conclusion

As you can see, creating a cross-platform PDF viewer that runs on both the mobile and the web using PDFTron SDK isn’t complicated when using PDFTron's Flutter SDK and WebViewer. And your new viewer will come out-of-the-box supporting hundreds of unique features, from annotation to redaction, which you can optionally add or remove using the APIs.

Get started with PDFTron for Flutter and WebViewer , and check out other WebViewer documentation to see your many options for customizing and extending your cross-platform viewer!

Let us know what you build. And if you have any questions or comments, don’t hesitate to contact us .