Enhancing PDF Functionality in Flutter Apps

Devanshi GargDevanshi Garg
7 min read

Introduction

The primary aim of this POC (Proof of Concept) is to demonstrate the capability of rendering PDF documents directly within the Flutter application itself, thereby eliminating the need for external applications. This feature enhances the app's autonomy and user experience.

  1. Enhanced Zooming Functionality: An essential aspect of the POC involves implementing robust zooming functionality while preserving high-resolution display to ensure crisp and clear rendering of text and images within the PDF viewer.

  2. Integrating Drawing Capabilities: A key objective of the POC is to incorporate drawing functionality directly into the PDF viewer. By enabling users to annotate or draw on the document, the application fosters greater interactivity and engagement, facilitating collaborative work and personalisation.

Findings

Despite the extensive research and trials, the POC encountered several challenges that hindered a fully successful implementation. Also, these findings are done mainly on Android but also follow for iOS. The findings are detailed below:

  1. Platform Channels:

    Platform channels in Flutter allow communication between Dart and the platform's native code (such as Android or iOS). This enables the use of native APIs/SDKs within a Flutter application. There are 3 types of platform channels : MethodChannel, EventChannel and BasicMessageChannel. For this POC, method channels were considered to integrate a native PDF viewer within the Flutter app.

    Create a Dart file inside lib folder:

// native_pdf_viewer.dart

import 'package:flutter/services.dart';

class NativePdfViewer {
  static const platform = MethodChannel('pdf_viewer_channel');

  Future<void> openPDF(String filePath) async {
    try {
      final result = await platform.invokeMethod('openPdf', {'path': filePath});
      print(result);
    } on PlatformException catch (e) {
      print("Failed to open PDF: '${e.message}'.");
    }
  }
}

Now, inside Android folder, follow this folder structure and follow as shown below:

// PdfViewerHandler.kt

package com.example.flutter_template

import androidx.core.content.FileProvider 
import android.content.ActivityNotFoundException 
import androidx.core.content.ContextCompat
import android.content.Context
import android.content.Intent
import android.net.Uri
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import java.io.File

class PdfViewerHandler(private val context: Context) : MethodCallHandler {
    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "openPdf" -> {
                val filePath = call.argument<String>("path")
                openPdfFile(filePath)
                result.success(null) 
            }
            else -> result.notImplemented()
        }
    }

    private fun openPdfFile(filePath: String?) {
        if (filePath == null) {
            // Handle error - file path not provided
            return
        }

        val file = File(filePath)
        val fileUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)

        val intent = Intent(Intent.ACTION_VIEW)
        intent.setDataAndType(fileUri, "application/pdf")
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)

        try {
            context.startActivity(intent)
        } catch (e: ActivityNotFoundException) {
            // Handle the case where no PDF viewer is found
        }
    }
}
// MainActivity.kt

package com.example.flutter_template

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import androidx.annotation.NonNull // For NonNull 
import com.example.flutter_template.PdfViewerHandler 



class MainActivity : FlutterActivity() {
     override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "pdf_viewer_channel")
                .setMethodCallHandler(PdfViewerHandler(this))
    }
}

Also, do not forget to check all the user read and write permissions inside AndroidManifest.xml.

Result : PDF is opening in a different app rather than within the Flutter application, it suggests that the PDF is being opened using an intent (Android) or a URL scheme (iOS) that directs the system to use the default PDF viewer application instead of rendering it within your own app.

  1. PlatformView : Rendering a native Android view inside a Flutter app is possible, but it requires embedding the native view into the Flutter widget tree. This can be achieved using the PlatformView widget in Flutter, which allows you to embed Android views directly into the Flutter UI.

    • A popular open-source PDF viewer, AndroidPdfViewer, was evaluated for this purpose, as we need to specify a package. In fact this is also being used underneath various famous pdf rendering packages in flutter like flutter_pdfview and pdfx. However, a critical limitation is its lack of annotation capabilities, as highlighted in its issues section.

    • Another PDF viewer considered was DrawOnPdf. This viewer supports drawing on PDFs but does not save the annotations. Saving annotations is complex, as it requires analyzing the correct pixels and page numbers where the user made changes. The lack of saving functionality makes it unsuitable for applications requiring persistent annotations.

Therefore, this approach was also discarded.

  1. pdfx: The pdfx package provided excellent zooming functionality and high-resolution rendering in an example application. But, since as mentioned above, pdfx uses AndroidPdfViewer underneath, it does not have capabilities to annotate.
import 'package:flutter/material.dart';
import 'package:pdfx/pdfx.dart';

class PdfViewerScreen extends StatefulWidget {
  const PdfViewerScreen({
    Key? key,
  }) : super(key: key);

  @override
  State<PdfViewerScreen> createState() => _PdfViewerScreenState();
}

class _PdfViewerScreenState extends State<PdfViewerScreen> {
  PdfControllerPinch? pdfPinchController;

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

  Future<void> _initializePdfController() async {
    pdfPinchController = PdfControllerPinch(
      document: PdfDocument.openAsset('assets/sample.pdf'),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Sample PDF"),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () {
            Navigator.of(context).pop();
          },
        ),
      ),
      body: PdfViewPinch(
        controller: pdfPinchController!,
        onDocumentError: print,
        onDocumentLoaded: print,
      ),
    );
  }
}

Achievements

The POC successfully demonstrated the rendering of PDFs within a Flutter application using the pdfx package, providing good zooming and high-resolution display. However, the implementation of annotation features remained unachieved.

Exploring Microsoft 365 PDF Viewer Integration

To overcome the limitations faced in the initial POC, the integration of the Microsoft 365 PDF Viewer via Android Method Channels was explored. This approach ensured that PDF files were opened using the Microsoft 365 PDF Viewer rather than any third-party application. The integration provided:

  • High-resolution rendering

  • Annotation capabilities

  • Robust zooming functionality

However, this method involves opening the PDF through a third-party app, which partially deviates from the goal of complete in-app autonomy.

// add these packages in pubspec.yaml
external_app_launcher: ^4.0.0
url_launcher: ^6.1.7

Inside lib folder create a file that connects method channel with Dart:

// create a file native_pdf_viewer and add this

import 'package:flutter/services.dart';

class NativePdfViewer {
  static const platform = MethodChannel('pdf_viewer_channel');

  Future<void> openPDF(String filePath) async {
    try {
      final result = await platform.invokeMethod('openPdf', {'path': filePath});
      print(result);
    } on PlatformException catch (e) {
      print("Failed to open PDF: '${e.message}'.");
    }
  }
}

To cater the UI, add below code inside main file:

// you can add this inside main file

Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: TextButton(
          onPressed: () async {
            bool isAppInstalled = await LaunchApp.isAppInstalled(
            androidPackageName: 'com.microsoft.office.officehubrow',
          );
          if (isAppInstalled) {
            NativePdfViewer().openPDF(filePath); // filepath will be your pdf filepath
          } else {
            final url = Uri.parse(
              'https://play.google.com/store/apps/details?id=com.microsoft.office.officehubrow',
            ); // will redirect to playstore if app is not already installed
            if (await canLaunchUrl(url)) {
              await launchUrl(
                url,
                mode: LaunchMode.externalApplication,
              );
            } else {
              print('Could not launch $url');
            }
          }
          },
          child: const Text("Open via Microsoft"),
        ),
      ),
    );
  }

Now, inside Android folder follow this structure as shown below:

// Inside MainActivity.kt

package com.example.pdf_viewer

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import androidx.annotation.NonNull // For NonNull 
import com.example.flutter_template.PdfViewerHandler 



class MainActivity : FlutterActivity() {
     override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "pdf_viewer_channel")
                .setMethodCallHandler(PdfViewerHandler(this))
    }
}
// Inside PdfViewerHandler.kt

package com.example.pdf_viewer

import androidx.core.content.FileProvider 
import android.content.ActivityNotFoundException 
import androidx.core.content.ContextCompat
import android.content.Context
import android.content.Intent
import android.net.Uri
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import java.io.File

class PdfViewerHandler(private val context: Context) : MethodCallHandler {
    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "openPdf" -> {
                val filePath = call.argument<String>("path")
                if (filePath.isNullOrEmpty()) {
                    result.error("NO_FILE_PATH", "The file path was not provided.", null)
                } else {

                    if (openPdfFile(filePath)) {
                        result.success(null)
                    } else {
                        result.error("NO_PDF_VIEWER_FOUND", "No PDF viewer found to open the file.", null)
                    }
                }
            }
            else -> result.notImplemented()
        }
    }

    private fun openPdfFile(filePath: String?): Boolean{
if (filePath.isNullOrEmpty()) return false

 val file = File(filePath)
        val fileUri: Uri = try {
            FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
        } catch (e: IllegalArgumentException) {
            // Handle error - file URI could not be generated
            return false
        }

        val viewIntent = Intent(Intent.ACTION_VIEW).apply {
            setDataAndType(fileUri, "application/pdf")
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            // Try to use the specified package first
            setPackage("com.microsoft.office.officehubrow")
        }


        return try {
            context.startActivity(viewIntent)
            true
        } catch (e: ActivityNotFoundException) {
            // Handle the case where no PDF viewer is found
            false
        }
    }
}

Conclusion

The POC provided valuable insights into rendering and interacting with PDFs within a Flutter application. While the pdfx package successfully addressed the zooming and resolution requirements, the annotation feature was not achievable within the Flutter app itself. Alternatively, leveraging the Microsoft 365 PDF Viewer offered a comprehensive solution but involved reliance on an external application.

0
Subscribe to my newsletter

Read articles from Devanshi Garg directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Devanshi Garg
Devanshi Garg