Producing PDFs within your  Flutter App 1/2

Photo by dlxmedia.hu on Unsplash

Producing PDFs within your Flutter App 1/2

·

7 min read

Introduction

Flutter is rapidly becoming a favored choice for cross-platform mobile app development due to its capacity to write code once and deploy it universally, ensuring consistent user experiences. Beyond its proficiency in crafting visually captivating interfaces, Flutter provides an array of dependable libraries and plugins to manage diverse tasks, such as the creation of PDF documents. This article delves into the process of generating PDF documents within a Flutter application, paving the way for the automated production of reports, invoices, and various document-based content.

Getting Started

To begin generating PDF documents in a Flutter app, we will use a popular package called PDF. This package provides a simple way to create PDF files programmatically.

Step 1: Create a New Flutter Project

Open your terminal or command prompt and create a new Flutter project:

flutter create my_pdf_app

Navigate to the project directory:

cd my_pdf_app

Step 2: Add the necessary packages

We need 3 packages for our simple example to work:

  1. pdf: helps to create a full multi-page document with graphics, images, and text using TrueType fonts

  2. path_provider: is for finding commonly used locations on the filesystem

  3. easy_pdf_viewer: helps us to view the pdf after it's generated

To add all these packages at once you run the following command

flutter pub add easy_pdf_viewer path_provider pdf

You can open your pubspec.yaml file to confirm whether the pdf package has been added as a dependency:

dependencies:
  flutter:
    sdk: flutter
  easy_pdf_viewer: ^1.0.7
  path_provider: ^2.1.1
  pdf: ^3.10.4

Step 3: Create the needed Classes/Files

  1. We first create a main_screen.dart file for our main page which we can place under screens directory for a clean app.

     import 'package:flutter/material.dart';
    
     class MainScreen extends StatefulWidget {
       const MainScreen({super.key});
    
       @override
       State<MainScreen> createState() => MainScreenState();
     }
    
     class MainScreenState extends State<MainScreen> {
    
       @override
       Widget build(BuildContext context) {
         return Scaffold(
           appBar: AppBar(
             backgroundColor: Theme.of(context).colorScheme.inversePrimary,
             title: const Text('My PDF App'),
           ),
           body: Center(
             child: ElevatedButton(
               onPressed: () async {
                 // our action of PDF Generation goes here
               },
               child: const Text('Generate PDF'),
             ),
           ),
         );
       }
     }
    
  2. Let's adjust ourmain.dart to point to the MainScreen class

     import 'package:flutter/material.dart';
    
     import 'screens/main_screen.dart';
    
     void main() {
       runApp(const MyApp());
     }
    
     class MyApp extends StatelessWidget {
       const MyApp({super.key});
    
       @override
       Widget build(BuildContext context) {
         return MaterialApp(
           title: 'My PDF App',
           theme: ThemeData(
             colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
             useMaterial3: true,
           ),
           home: const MainScreen(),
         );
       }
     }
    
  3. Lets create a pdf_utils.dartfile under utils directory

     import 'dart:io';
    
     class PdfUtils {
       PdfUtils._();
    
       static Future<File> generatePDF() async {
         final pdfFile = File();
       }
     }
    

Step 4: Create a PDF Document

Next, improve our generatePDF function so that it can be to generate a PDF document as we intended.

import 'dart:io';

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

class PdfUtils {
  PdfUtils._();  
  static Future<File> generatePDF() async {
    final pdfFile = File();

    pdf.addPage(
      pw.Page(
        margin: const pw.EdgeInsets.all(10),
        pageFormat: PdfPageFormat.a4,
        build: (pw.Context context) {
          return pw.Center(
            child: pw.Text(
              "This is a text!",
              style: pw.TextStyle(
                fontSize: 30,
                color: PdfColors.red900,
                fontWeight: pw.FontWeight.bold,
              ),
            ),
          );
        },
      ),
    );
    return pdfFile;
  }
}

Let's break down the code step by step:

  1. Import Statements: The import statements bring in the necessary libraries and packages for working with PDFs. dart:io is imported for file manipulation, and the pdf package is used for creating and manipulating PDF documents. The as pw alias is used to make it easier to work with the pdf package's widgets.

  2. PDF Generation:

       pdf.addPage(
         pw.Page(
           margin: const pw.EdgeInsets.all(10),
           pageFormat: PdfPageFormat.a4,
           build: (pw.Context context) {
             return pw.Center(
               child: pw.Text(
                 "This is a text!",
                 style: pw.TextStyle(
                   fontSize: 30,
                   color: PdfColors.red900,
                   fontWeight: pw.FontWeight.bold,
                 ),
               ),
             );
           },
         ),
       );
    

    This code snippet adds a page to the PDF document using the pdf package's addPage method. It defines the page's properties, such as margins, page format (A4 in this case), and the content to be displayed on the page. In this example, the page contains a centered text element with specific styling.

    The build function defines the content of the page. In this case, it returns a pw.Center widget containing a text element.

This code however when you run will not show anything until you do the next actions outline below.

Step 5: Saving the PDF Document to Storage

We will need to improve the generatePDF function so that it can first of all save the generated PDF

import 'dart:io';
import 'dart:developer' as logger show log;

import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;

class PdfUtils {
  PdfUtils._();

  static Future<File> generatePDF() async {
    final pdf = pw.Document();
    // fetching a directory to save our pdf to
    Directory filePath = await getApplicationDocumentsDirectory();

    pdf.addPage(
      pw.Page(
        margin: const pw.EdgeInsets.all(10),
        pageFormat: PdfPageFormat.a4,
        build: (pw.Context context) {
          return pw.Center(
            child: pw.Text(
              "This is a text!",
              style: pw.TextStyle(
                fontSize: 30,
                color: PdfColors.red900,
                fontWeight: pw.FontWeight.bold,
              ),
            ),
          );
        },
      ),
    );

    final pdfFile = File('${filePath.path}/my_example.pdf');
    logger.log('Saved to: ${pdfFile.path}');
    await pdfFile.writeAsBytes(await pdf.save());
    return pdfFile;
  }
}

This Dart code defines a utility class called PdfUtils that generates a PDF document using the pdf package and saves it to the device's file system. Let's break down the code step by step:

  1. Importing Dependencies where I have added 2 more:

    • dart:developer as logger: This alias imports the log function from the dart:developer library, allowing you to log messages.

    • package:path_provider/path_provider.dart: This package is used to get access to the device's file system, specifically the application's document directory.

  2. Creating the PDF Document:

    • It starts by creating a new pw.Document object, which will represent our PDF document.
  3. Getting the Document Directory:

    • getApplicationDocumentsDirectory() is an asynchronous function from the path_provider package. It retrieves the application's document directory, which is a platform-specific directory where you can store user data. This directory is a suitable location for saving user-generated files like databases and in this case PDFs.
  4. Defining the PDF File Path:

    • It specifies the file path where the generated PDF will be saved using File('${filePath.path}/my_example.pdf'). The filePath variable contains the path to the application's document directory.
  5. Logging the File Path:

    • The code logs the path where the PDF file will be saved using logger.log(). This can be helpful for debugging purposes to know where the file is stored.
  6. Saving and Returning the PDF:

    • It saves the PDF content to the specified file path using await pdfFile.writeAsBytes(awaitpdf.save()). The pdf.save() method returns the PDF content as a list of bytes, which is then written to the file.
  7. Returning the File:

    • Finally, the method returns the File object representing the saved PDF file.

Step 6: Displaying the PDF Document

To use this PdfUtils class in the utils directory, I have to call the generatePDF() method, and it will generate a PDF document with the specified content and save it to the application's document directory. I can then use this generated PDF file as needed in my app. The modified main_screen.dart will now look like this:

import 'package:easy_pdf_viewer/easy_pdf_viewer.dart';
import 'package:flutter/material.dart';

import '../utils/pdf_utils.dart';

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State<MainScreen> createState() => MainScreenState();
}

class MainScreenState extends State<MainScreen> {
  late PDFDocument pdfDoc;

  void openPdfViewer() {
    final pdfViewer = PDFViewer(document: pdfDoc);
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => pdfViewer),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('My PDF App'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            final pdf = await PdfUtils.generatePDF();
            pdfDoc = await PDFDocument.fromFile(pdf);
            openPdfViewer();
          },
          child: const Text('Generate PDF'),
        ),
      ),
    );
  }
}

In this code, when I press the "Generate PDF" button, the generatePDF function is called, which generates the PDF document. To display the PDF, you can use packages like easy_pdf_viewer, which allow users to view PDF files within the app in a separate screen called the PDFViewer.

Conclusion

As you have seen generating a PDF Document is as easy as writing a normal widget tree except that you have to work with pw.Document in all your widgets. The usual widgets you are used to have to be derived from the pw which is an alias of the pdf package. Also you won't need write permissions on your device be it Android or iOS if you intend to save the PDF on the application's document directory, which is a platform-specific directory.


You can find the source code for the app I built when writing this tutorial on Github:

My PDF App Example

Slides for this Article


In the next article which will be a continuation of this one, I will address producing advanced PDF Documents that involve more than one page with tables. Happy Coding!