Paul G
Paul G

Reputation: 23

Displaying a PDF in Flutter

I'm brand new to Flutter and am struggling to display a PDF from a Base64 string. I can get it to work, but only with a pointless UI sitting between the list and the PDF.

The app simply crashes with 'Lost connection to device' and no other information appears in the Android Studio debug console.

So, this works:

import 'dart:developer';
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_full_pdf_viewer/full_pdf_viewer_scaffold.dart';
import 'package:path_provider/path_provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class PdfBase64Viewer extends StatefulWidget {
  final DocumentSnapshot document;

  const PdfBase64Viewer(this.document);
  @override
  _PdfBase64ViewerState createState() => new _PdfBase64ViewerState();
}

class _PdfBase64ViewerState extends State<PdfBase64Viewer> {
  String pathPDF = "";

  @override
  void initState() {
    super.initState();
    createFile().then((f) {
      setState(() {
        pathPDF = f.path;
        print("Local path: " + pathPDF);
      });
    });
  }

  Future<File> createFile() async {
    final filename = widget.document.data()['name'];
    var base64String = widget.document.data()['base64'];
    var decodedBytes = base64Decode(base64String.replaceAll('\n', ''));

    String dir = (await getApplicationDocumentsDirectory()).path;
    File file = new File('$dir/$filename');
    await file.writeAsBytes(decodedBytes.buffer.asUint8List());

    return file;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('This UI is pointless')),
      body: Center(
        child: RaisedButton(
          child: Text("Open PDF"),
          onPressed: () => Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => PDFScreen(pathPDF)),
          ),
        ),
      ),
    );
  }
}

class PDFScreen extends StatelessWidget {
  String pathPDF = "";
  PDFScreen(this.pathPDF);

  @override
  Widget build(BuildContext context) {
    return PDFViewerScaffold(
        appBar: AppBar(
          title: Text("Document"),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.share),
              onPressed: () {},
            ),
          ],
        ),
        path: pathPDF);
  }
}

But this doesn't:

import 'dart:developer';
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_full_pdf_viewer/full_pdf_viewer_scaffold.dart';
import 'package:path_provider/path_provider.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

class PdfBase64Viewer extends StatefulWidget {
  final DocumentSnapshot document;

  const PdfBase64Viewer(this.document);
  @override
  _PdfBase64ViewerState createState() => new _PdfBase64ViewerState();
}

class _PdfBase64ViewerState extends State<PdfBase64Viewer> {
  String pathPDF = "";

  @override
  void initState() {
    super.initState();
    createFile().then((f) {
      setState(() {
        pathPDF = f.path;
        print("Local path: " + pathPDF);
      });
    });
  }

  Future<File> createFile() async {
    final filename = widget.document.data()['name'];
    var base64String = widget.document.data()['base64'];
    var decodedBytes = base64Decode(base64String.replaceAll('\n', ''));

    String dir = (await getApplicationDocumentsDirectory()).path;
    File file = new File('$dir/$filename');
    await file.writeAsBytes(decodedBytes.buffer.asUint8List());

    return file;
  }

  @override
  Widget build(BuildContext context) {
    return PDFViewerScaffold(
        appBar: AppBar(
          title: Text("Document"),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.share),
              onPressed: () {},
            ),
          ],
        ),
        path: pathPDF);
  }
}

Can anyone help me understand why?

Upvotes: 2

Views: 2964

Answers (1)

Uroš
Uroš

Reputation: 1658

You should pay more attention to what the output is (or isn't), it will make your debugging process a lot faster and without needing SO most of the time.

The app simply crashes with 'Lost connection to device' and no other information appears in the Android Studio debug console.

So that means you aren't seeing print("Local path: " + pathPDF); Which means your app hasn't made it that far.


As to why it is crashing, welp without any testing I can see you aren't handling your asyncs and futures properly. You are just assuming they will be ok and you are assuming they will finish quickly (before the widget gets built). Your PDFViewerScaffold is most probably getting an empty string. A simple check can fix that:

  @override
  Widget build(BuildContext context) {
    return (pathPDF == null || pathPDF.isEmpty) ? Center(child: CircularProgressIndicator())
    : PDFViewerScaffold( //....

For a really nice and clean code you should move pdf generation stuff into a separate class (or at least file) and you should also take into consideration how much time is acceptable for such a task (timeout).


PS The reason why your code works with "pointless UI" is because your fingers are slower than file generation, by the time you've tapped on RaisedButton the file has been already created and luckily has a path. That first code also doesn't do any checks, so if file creation fails for whatever reason you'd most probably end up with a crash or a blank screen.


EDIT: Bellow is a full example of how I would go about doing this. It should make your app a lot more resilient to crashes.

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:flutter_full_pdf_viewer/full_pdf_viewer_scaffold.dart';
import 'package:path_provider/path_provider.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PdfBase64Viewer(),
    );
  }
}

Future<File> createFile(String base64String) async {
  final fileName = "test";
  final fileType = "pdf";
  var decodedBytes = base64Decode(base64String);
  String dir = (await getApplicationDocumentsDirectory()).path;

  // uncomment the line bellow to trigger an Exception
  // dir = null;

  File file = new File('$dir/$fileName.$fileType');

  // uncomment the line bellow to trigger an Error
  // decodedBytes = null;

  await file.writeAsBytes(decodedBytes.buffer.asUint8List());

  // uncomment the line bellow to trigger the TimeoutException
  // await Future.delayed(const Duration(seconds: 6),() async {});

  return file;
}

class PdfBase64Viewer extends StatefulWidget {
  const PdfBase64Viewer();
  @override
  _PdfBase64ViewerState createState() => new _PdfBase64ViewerState();
}

class _PdfBase64ViewerState extends State<PdfBase64Viewer> {
  bool isFileGood = true;
  String pathPDF = "";
  /*
  Bellow is a b64 pdf, the smallest one I could find that has some content and it fits within char limit of StackOverflow answer.
  It might not be 100% valid, ergo some readers might not accept it, so I recommend swapping it out for a proper PDF.
  Output of the PDF should be a large "Test" text aligned to the center left of the page.
  source: https://stackoverflow.com/questions/17279712/what-is-the-smallest-possible-valid-pdf/17280876
  */
  String b64String = """JVBERi0xLjIgCjkgMCBvYmoKPDwKPj4Kc3RyZWFtCkJULyA5IFRmKFRlc3QpJyBFVAplbmRzdHJlYW0KZW5kb2JqCjQgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL1BhcmVudCA1IDAgUgovQ29udGVudHMgOSAwIFIKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL0tpZHMgWzQgMCBSIF0KL0NvdW50IDEKL1R5cGUgL1BhZ2VzCi9NZWRpYUJveCBbIDAgMCA5OSA5IF0KPj4KZW5kb2JqCjMgMCBvYmoKPDwKL1BhZ2VzIDUgMCBSCi9UeXBlIC9DYXRhbG9nCj4+CmVuZG9iagp0cmFpbGVyCjw8Ci9Sb290IDMgMCBSCj4+CiUlRU9G""";
  @override
  void initState() {
    super.initState();
    Future.delayed(Duration.zero,() async {
      File tmpFile;
      try {
         tmpFile = await createFile(b64String).timeout(
            const Duration(seconds: 5));
      } on TimeoutException catch (ex) {
        // executed when an error is a type of TimeoutException
        print('PDF taking too long to generate! Exception: $ex');
        isFileGood = false;
      } on Exception catch (ex) {
        // executed when an error is a type of Exception
        print('Some other exception was caught! Exception: $ex');
        isFileGood = false;
      } catch (err) {
        // executed for errors of all types other than Exception
        print("An error was caught! Error: $err");
        isFileGood = false;
      }
      setState(() {
        if(tmpFile != null){
          pathPDF = tmpFile.path;
        }
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return (!isFileGood) ? Scaffold(body: Center(child: Text("Something went wrong, can't display PDF!")))
    : (pathPDF == null || pathPDF.isEmpty) ? Scaffold(body: Center(child: CircularProgressIndicator()))
    : PDFViewerScaffold(
        appBar: AppBar(
          title: Text("Document"),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.share),
              onPressed: () {},
            ),
          ],
        ),
        path: pathPDF);
  }
}

Upvotes: 1

Related Questions