flutter中pdf的生成、预览、分享实现

公司最近的app新版本需求里有个pdf报告的部分,为了减少工作量,决定使用支持跨平台的flutter来开发这个模块
这里记录一下开发各个阶段遇到的问题,以及自己的各种解决方案,及每个方案的优劣和我自己的心路历程
作为一个前端,在此之前虽然听说过flutter,但还从未学习过,从零开始用这样的新兴技术上操刀,其实真个心路历程都是痛苦的,至于代码方面,也是由开始的一坨shit,慢慢变成了半坨shit,虽然有进步,但是这些代码各位看官也就看看就行了
最终成品是用的flutter_html_to_pdf插件生成pdf,pdfx预览pdf,原生调用分享,整体来说代码相对比较干净

1|0pdf的生成

1|1方案历程

1|2暴力截图

  • 暴力截图的本质是对页面的根组件进行定位,然后将该组件整个按比例生成一张图片
  • 所以截图的前提是有页面,所以这个方案其实对应了预览部分的第一个方案,需要先用flutter语法搭建pdf页面
  • flutter搭建页面用的是dart语法,总体来说不算难,但是现学现卖,写出来的代码就是shit,这里就不贴了,但是总体上搭出来的pdf页面还是比较完美的,毕竟dart也支持flex布局,有这个东西,对于一个前端来说,写个pdf挺简单的
  • 有了页面,那么接下来就是截图了
    • 首先需要定位目标跟组件,也就是不带页面appBar部分的纯pdf部分。flutter中有个叫globalKey的对象,是一个具有全局唯一性的对象,用它可以绑定页面中任意组件,并在页面渲染之后的任意时刻获取该绑定组件的状态、结构
    • 用法:
      先在HomePageState里声明一个全局key: GlobalKey mykey = new GlobalKey();
      然后再pdf组件的根Container里添加Container(key: mykey, child: PdfContainer())
      最后再需要用到该组件时,通过mykey.currentContext即可获取当前组件,重点注意,要获取到mykey绑定的组件,前提是已经成功绑定了,也就是说,页面mounted之后,在这之前获取到的是空,所以如果报错mykey.currentContext为null,请排查并判断自己使用mykey.currentContext的时候,组件是否已经绑定
    • 定位到了组件,然后就是生成图片了,这里主要用了Image.memory对二进制数据进行图片化
// 这里如果直接对widget截图生成图片,会导致widget里的图片看不见,因为这时图片还没加载完毕,需要手动缓存 Future<Image> createScreenShot() async { AssetImage imageCache = AssetImage('images/pdfLogo.png'); await precacheImage(imageCache , context); await Future.delayed(Duration(milliseconds: 100)); // delay 100ms确保图片缓存好了 // 获取globalkey定位的根组件 RenderRepaintBoundary boundary = globalkey.currentContext!.findRenderObject() as RenderRepaintBoundary; // 生成二进制数据 ui.Image image = await boundary.toImage(pixelRatio: ui.window.devicePixelRatio); ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png); Uint8List picBytes = byteData!.buffer.asUint8List(); setState(() { imageBytes = picBytes; imageGenerateNotDone = false; // 标志图片生成完成,loading组件就可以停了,也可以开始生成pdf了 }); return Image.memory(imageBytes, fit: BoxFit.cover); }
  • 但是这里的生成函数是一个Future函数,这是为了配合loading组件的使用,后面再细说
  • 图片生成好了,就用pdf插件放一张图就好了,比较简单,就不放代码了

1|3pdf插件语法搭建

  • 明明是官方插件,也不知道是哪个大哥写的,其实和dart语法真的很像,但是却又因为不一样,需要和页面分开重新搭建一个pdf结构,虽然整体结构和dart搭建的页面结构基本一模一样,只是组件名不一样,然后会有一些属性不兼容,所以搭建挺快的,但是还是不妨碍我说它sb,就非常后悔选用这个。
  • 虽然sb,但是还是完整的记录下自己的踩坑历程
  • pdf插件生成pdf文件的过程就是搭建的过程,先声明一个pdf = pw.Document(),然后pdf.addPage()就可以开始按照dart搭页面的方式搭pdf了,就不贴代码了,直接复制dart搭建的页面的代码,然后改成pw的widget就行了,相对简单
  • 但是pdf插件有一个很大的坑
    • 这个插件不能识别除了英文以外的任何语言,需要导入字体库,这可以说非常的机车
    • 而我们的app要兼容7种语言,还有regular和bold两种字重,而且还需要考虑使用的字体库的版权问题,是否能免费商用,然后找到了合适的字体库,发现总共有20+M,直接导致了APP打包以后也变大了20+M,这是产品那边基本不能接受的,所以第一期先这么做,毕竟不熟,但是这个pdf插件的方案肯定是需要替换的
    • 这也顺道引出了flutter中多语言配置的使用,链接传送门

1|4flutter_html_to_pdf插件

  • 这也是项目最终使用的生成插件,用html搭建pdf页面,非常契合我web前端的身份,但是也意味着需要重新搭pdf页面了
  • 这个插件也有几个比较坑的地方
    • 第一个坑:有数据插入的页面不能直接读html文件,就算在html文件中写了插值,也不能实现,基本写法如下
    //all page html var myHtmlStr = """ <!DOCTYPE html> <html> <head> </head> <body> <div class="container"> ${header} <div class="content"> ${detailHtml} ${summary} </div> </div> </body> ${scriptHtml} ${cssHtml} </html> """;
    • 可见,这里的html标签全是写成了字符串,然后用${}进行插值,不过这个插件厉害的地方在于,它能够识别js语法,如上,我这里是插入了一个scriptHtml的,所以我们可以在js里画图或者操作dom之类的
    • 第二个坑:html里引用图片不能直接通过项目路径来访问,而需要通过打包后的app路径来访问
    • 先来看看插件官方文档的说法
    <!--If your want to add local image from device to your HTML you need to pass path to image as src value.--> <img src="file:///storage/example/your_sample_image.png" alt="web-img"> <!--or if you want to use the image File object.--> <img src="${imageFile.path}" alt="web-img"> <!-- or you can just put your images onto a cdn server, and use its uri link.--> <img src="https://i.imgur.com/wxaJsXF.png" alt="web-img">
    • 可见有三种引用图片的方式,第三种用url链接的方式访问很直观,也简单,要是公司有自己的cdn服务器可以直接用这个,但是公司没有,也不好放到免费的cdn上,所以看看前两种吧。其实前两种都涉及一个问题,我们需要先确认,这里引用的路径到底是项目静态资源路径,还是app路径,尝试一下项目资源路径就知道,是引用不了的,所以我们尝试一下app路径能不能引用到,但是app的可供访问的的storage里没有我们的图片,我们还得先把图片放进去
    saveImgsToAppdir() async { // 原理简单,就是先从项目资源里把图片读出来,然后写进appdir的新建图片文件里 ByteData bytes1 = await rootBundle.load("images/pdfLogo.png"); File pngFile1 = File("${appDir.path}/pdfLogo.png"); pngFile1.writeAsBytes(bytes1.buffer.asUint8List()); }
    • 然后appdir里有我们需要用的图片了,就可以通过src="file:///${appDir.path}/pdfLogo.png"的写法引入图片了,注意前面的file:///
    • 至于appdir:appDir = await getApplicationDocumentsDirectory();存个全局的就行了
    • 第三个也不能算坑,就是提高效率的东西,dart不能像我们用vue这些框架一样用if和for标签进行渲染,但是我们可以用“组件化”的方式,用dart也实现这个写法,比如:
    // 写一个assessmentBlockHtmlfy组件,然后再其他地方直接调 String assessmentBlockHtmlfy(ObesityData ObesityData) { var assessmentItems = ""; for (var seg in ObesityData.segs) { assessmentItems += """ <div class="assessmentItem"> <div class="assessVal textHoriCenter"></div> <div class="assessType textHoriCenter">${seg}</div> </div> """; } return """ <div class="assessmentBlock" data-index="${ObesityData.state}" data-val="${ObesityData.val}"> <div class="assessmentTitle">${ObesityData.title}</div> <div class="greyBox assessmentItems"> ${assessmentItems} </div> </div> """; }

2|0pdf的预览

2|1方案历程

2|2flutter_full_pdf_viewer插件

  • 首先预览pdf文件都需要注意一个问题,打开文件之前pdf必须生成完了,所以这就需要一个loading组件来给个缓冲,前文也提到了生成pdf我们用的是Future函数,就是在这里用到
// 这里的loading用的是flutter_easyloading插件 // 在根Widget组件MaterialApp的地方初始化loading MaterialApp( home: MyHomePage(title: "flutter"), builder: EasyLoading.init(), // loading声明 ... ) // 声明了还没有显示,在statefulWidget初始化的地方显示loading @override void initState() { super.initState(); EasyLoading.show(status: ''); // status是loading提示文字 } // 最后在pdf生成完成后再隐藏loading Future<void> generateAndNavigate() async { await generateHtmlToPdf(pdfFileName); // await Future.delayed(Duration(milliseconds: 2000)); // 演示2秒测试loading setState(() { isPdfNotDone = false; }); EasyLoading.dismiss(); }
  • 目前看下来好像这个插件没啥问题啊,为啥被放弃了呢?大概就是不符合我们的使用场景吧,我们的app的其他部分都还是用的原生的android和ios搭建的,只有pdf这个页面用了flutter,所以现在的场景是,从原生点击入口进入到我的flutter页面,pdf的预览就是首页,而这个插件预览pdf,他是需要一个跳转操作的,即在当前页面通过一个入口,触发后通过navigator跳转到预览pdf的页面,这样就会多一个页面跳转的动画,也会导致返回上一页即返回原生页面出bug,所以被放弃了。
  • 如果你的使用场景是通过如果跳转预览文件,可以使用这个插件

2|3flutter搭建页面

  • 纯dart语法搭建一个页面,这个做法比较粗暴,也简单,就是工作量较大,因为我们已经使用了html搭建pdf结构,所以还需要重新搭建一套dart的pdf结构
  • 这个做法的优点在于不用loading,进flutter页面直接预览
  • 但是这个做法一个非常大的弊端在于,无法缩放、拖拽pdf,要做手势判断其实也行,但是其工作量非常巨大,因为会导致组件布局变形,这是得不偿失的,而由于不能缩放,在产品那边不过关,被卡掉了

2|4预览图片

  • dart搭建的pdf页面都搭好了,不想做无用功,就结合前面的对跟组件截图生成图片的方式,为啥不把这个页面改成一张图呢,如果是图不就好做缩放了吗?说干就干
  • 截图方式
  • 然后将页面替换成生成的图片简单,问题是图片的缩放、拖拽的实现,传送门
  • 这种方式的预览,到测试那儿被说不够流畅,用户体验不太好,还是需要改动

2|5pdfx插件

  • 最后用了pdfx插件,这个插件就没有flutter_full_pdf_viewer插件的跳转问题,直接是一个PdfController来展示预览的文件
  • 在生成之前,除了loading,我们还需要一个空Container来替换PdfController
body: isPdfNotDone ? Container() : WillPopScope( onWillPop: _requestPop, child: PdfView( controller: PdfController( document: PdfDocument.openFile("${appDir.path}/${pdfFileName}.pdf") ), renderer: (PdfPage page) => page.render( // 这里是设置pdf预览的清晰度,和pdf文件本身无关,只是预览时的清晰度 width: page.width * 6, height: page.height * 6 ), pageBuilder: ( Future<PdfPageImage> pageImage, int index, PdfDocument document ) => PhotoViewGalleryPageOptions( // 这里是设置缩放的最大最小值 imageProvider: PdfPageImageProvider(pageImage, index, document.id), maxScale: PhotoViewComputedScale.contained * 4.0, minScale: PhotoViewComputedScale.contained * 1.0 ) ) )
  • 这里有个WillPopScope组件,是flutter中用来触发手机的返回事件的,如右滑手机屏幕,还有手机的返回键,这些触发的返回事件,传送门
  • pdfx插件整体来说挺不错的,也没遇到啥坑

3|0pdf的分享

  • 这个比较简单,从始至终实现了就没变过,调原生分享通道,flutter调用原生通道有两种,一种是EventChannel,一种是MethodChannel
// 先来看MethodChannel,分享通道只是传了一个pdf文件的路径给原生,然后他们去打开文件MethodChannel是直接触发的 const platform = MethodChannel("monitorFlutterChannel"); try { var map = {"filePath": "${appDir.path}/${fileName}"}; final int res = await platform.invokeMethod("flutterToNative", map); } on PlatformException catch (e) { print(e); } // 而EventChannel则是需要监听的,而且需要先注册,如原生进flutter页面传递数据触发的事件 // 先在根state中注册监听原生通道 EventChannel eventChannel = EventChannel('monitorNativeChannel'); // 然后声明一个事件处理函数handler,和错误处理函数errorHandler void handler(var para) { setState(() { doSth(); }); } void errorHandler(Object error) { print(error); } // 最后监听就行了 @override void initState() { // 实现通道的监听,并传入两个带有参数的函数用于监听到数据后 对数据进行处理 eventChannel .receiveBroadcastStream() .listen(handler, onError: errorHandler); ··· }

__EOF__

本文作者Mizuki
本文链接https://www.cnblogs.com/mizuki-vone/p/16373416.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   Mizuki-Vone  阅读(6456)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示