在 Flutter 中显示 PDF

Posted

技术标签:

【中文标题】在 Flutter 中显示 PDF【英文标题】:Displaying a PDF in Flutter 【发布时间】:2021-03-28 10:42:11 【问题描述】:

我是 Flutter 的新手,正在努力从 Base64 字符串显示 PDF。我可以让它工作,但只有在列表和 PDF 之间有一个毫无意义的 UI。

应用程序只是崩溃并显示“与设备的连接丢失”,并且 android Studio 调试控制台中没有其他信息。

所以,这行得通:

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);
  

但这不是:

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);
  

谁能帮我理解为什么?

【问题讨论】:

【参考方案1】:

您应该更加注意输出是(或不是)什么,这将使您的调试过程更快,并且大部分时间都不需要。

该应用程序只是崩溃并显示“与设备的连接丢失”而没有其他 信息出现在 Android Studio 调试控制台中。

这意味着你没有看到print("Local path: " + pathPDF); 这意味着你的应用还没有走到那一步。


至于它为什么会崩溃,没有任何测试,我可以看到你没有正确处理你的asyncs and futures。您只是假设它们会没事,并且假设它们会很快完成(在构建小部件之前)。您的 PDFViewerScaffold 很可能得到一个空字符串。一个简单的检查就可以解决这个问题:

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

对于一个非常漂亮和干净的代码,您应该将生成 pdf 的内容移到一个单独的类(或至少是文件)中,并且您还应该考虑这样的任务可以接受多少时间 (timeout)。


PS 你的代码使用“无意义的 UI”的原因是因为你的手指比文件生成慢,当你点击 RaisedButton 时,文件已经创建并且幸运的是有一个路径。第一个代码也不做任何检查,所以如果文件创建由于任何原因失败,您很可能会以崩溃或空白屏幕告终。


编辑: Bellow 是一个完整的例子,说明了我将如何做这件事。它应该使您的应用在崩溃时更具弹性。

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 *** 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://***.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);
  

【讨论】:

我确实看到路径在看到与设备的连接丢失之前立即回显,这就是为什么我为此挠头的原因。这是一个快速 POC,可以查看更广泛的应用程序是否可行。我将删除未来,看看是否能解决它。 不,这不是未来 - pathPDF 设置正确。 这正在工作。我没有删除未来,我测试了 null,如果它为 null,则返回不同的视图。当然,它从来都不是空的,它是空的。使用您的支票修复它!谢谢。 不客气。我在答案中添加了一个编辑以包含一个完整的示例,它还进行了一些改进,可以使您的应用程序不太容易出现故障和崩溃。一切顺利。

以上是关于在 Flutter 中显示 PDF的主要内容,如果未能解决你的问题,请参考以下文章

如何在单页#flutter中显示网格视图和其他视图

在 Flutter 的 GridView 中显示图像?

如何在 Flutter 应用程序中显示原生 Android 视图?

在 Flutter 的登录屏幕中显示循环进度对话框,如何在 Flutter 中实现进度对话框?

在 Flutter 中显示来自 API 的数据

Flutter - 如何在 SimpleDialog 框中显示 GridView?