乐趣区

关于flutter:用-Flutter-和-Firebase-轻松构建-Web-应用

作者 / Very Good Ventures Team

咱们 (Very Good Ventures 团队) 与 Google 单干,在往年的 Google I/O 大会上推出了 照相亭互动体验 (I/O Photo Booth)。您能够与深受青睐的 Google 吉祥物合影: Flutter 的 Dash、Android Jetpack、Chrome 的 Dino 和 Firebase 的 Sparky,并用各种贴纸装璜照片,包含派对帽、披萨、时尚眼镜等。当然,您也能够通过社交媒体下载并分享,或者用作您的个人头像!

△ Flutter 的 Dash、Firebase 的 Sparky、Android Jetpack 和 Chrome 的 Dino

咱们应用 Flutter web 和 Firebase 构建了 I/O 照相亭。因为 Flutter 当初反对打造 Web 利用,咱们认为这将是一个很好的形式,能够让世界各地的与会者在往年的线上 Google I/O 大会上轻松拜访这一利用。Flutter web 打消了必须通过利用商店装置利用的阻碍,同时用户还能够灵便抉择运行利用的设施: 挪动设施、桌面设施或平板电脑。因而,只有能应用浏览器,用户便可无需下载间接应用 I/O 照相亭。

只管 I/O 照相亭旨在提供 Web 体验,但所有代码均采纳与平台无关的架构编写而成。当相机插件等原生性能的反对在各个平台就绪后,这套代码即可在所有平台 (桌面、Web 和挪动设施) 通用。

应用 Flutter 构建虚构照相亭

构建 Web 版 Flutter 相机插件

第一个挑战即在 Web 上为 Flutter 构建摄像头插件。最后,咱们分割了 Baseflow 团队,因为他们负责保护现有的开源 Flutter 摄像头插件。Baseflow 致力于构建实用于 iOS 和 Android 的一流摄像头插件反对,咱们也很乐于与其单干,应用 联结插件 办法为插件提供 Web 反对。咱们尽可能合乎官网插件接口,以便咱们能够在准备就绪时将其合并回官网插件。

咱们确定了两个对于在 Flutter 中构建 I/O 照相亭相机体验至关重要的 API。

  • 初始化摄像头: 利用首先须要拜访您的设施摄像头。对于桌面设施,拜访的可能是网络摄像头,而对于挪动设施,咱们抉择了拜访前置摄像头。咱们还提供了 1080p 的预期分辨率,以依据用户设施类型充沛进步拍摄品质。
  • 拍照: 咱们应用了内置的 HtmlElementView,该控件应用平台视图将原生 Web 元素渲染为 Flutter widget。在此我的项目中,咱们将 VideoElement 渲染为原生 HTML 元素,这便是您在拍照前会在屏幕上看到的内容。咱们还应用了一个 CanvasElement,用于在您点击拍照按钮时从媒体流中捕捉图像。
Future<CameraImage> takePicture() async {
 final videoWidth = videoElement.videoWidth;
 final videoHeight = videoElement.videoHeight;
 final canvas = html.CanvasElement(
   width: videoWidth,
   height: videoHeight,
 );
 canvas.context2D
   ..translate(videoWidth, 0)
   ..scale(-1, 1)
   ..drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight);
 final blob = await canvas.toBlob();
 return CameraImage(data: html.Url.createObjectUrl(blob),
   width: videoWidth,
   height: videoHeight,
 );
}

摄像头权限

在 Web 上实现 Flutter 摄像头插件后,咱们创立了一个形象布局,以依据相机权限显示不同的界面。例如,在期待您容许或回绝应用浏览器摄像头时,或者如果没有可供拜访的摄像头时,咱们能够显示一条说明性音讯。

Camera(
 controller: _controller,
 placeholder: (_) => const SizedBox(),
 preview: (context, preview) => PhotoboothPreview(
   preview: preview,
   onSnapPressed: _onSnapPressed,
 ),
 error: (context, error) => PhotoboothError(error: error),
)

在下面的形象布局中,placeholder 会在利用期待您授予摄像头权限时返回初始界面。Preview 则会在您授予权限后返回实在的界面,并显示摄像头的实时视频流。结尾的 Error 结构语句则能够在谬误产生时捕捉谬误并显示相应的音讯。

生成镜像照片

咱们的下一个挑战是生成镜像照片。如果咱们照原样应用摄像头拍摄的照片,那么您看到的内容将与您在照镜子时所看到的内容不一样。某些设施会提供专门解决这一问题的设置选项,所以,如果您用前置摄像头拍照,您看到的其实是照片的镜像版本。

在咱们的第一种办法中,咱们尝试捕获默认的摄像头视图,而后围绕 y 轴对其进行 180 度翻转。这种办法仿佛无效,但起初咱们遇到了 一个问题,即 Flutter 偶然会笼罩这个翻转,导致视频复原到未镜像的版本。

在 Flutter 团队的帮忙下,咱们将 VideoElement 放在 DivElement 中,并更新 VideoElement 以填充 DivElement 的宽度和高度,解决了这个问题。这样一来,咱们可能为视频元素利用镜像,同时因为父元素是 div,所以不会被 Flutter 笼罩翻转成果。如此一来,咱们便取得了所需的镜像摄像头视图!

△ 未镜像的视图

△ 镜像视图

放弃宽高比

在大屏幕上放弃 4:3 宽高比,以及在小屏幕上放弃 3:4 宽高比,这个操作起来比看起来更难!放弃宽高比十分重要,既要合乎 Web 利用的整体设计,又要确保在社交媒体上分享照片时,令其中的像素呈现出清晰的本色成果。这是一项具备挑战性的工作,因为不同设施上内置摄像头的宽高比差别很大。

为了强制放弃宽高比,利用首先应用 JavaScript getUserMedia API 从设施摄像头申请可能的最大分辨率。随后,咱们将此 API 传递到 VideoElement 流中,这便是您在摄像头视图中看到的内容 (当然是已镜像的版本)。咱们还利用了 object-fit CSS 属性来确保视频元素能盖住其父级容器。咱们应用 Flutter 自带的 AspectRatio widget 来设置宽高比。因而,摄像头不会对显示的宽高比做出任何假如;它始终返回反对的最大分辨率,而后恪守 Flutter 提供的约束条件 (在本例中为 4:3 或 3:4)。

final orientation = MediaQuery.of(context).orientation;
final aspectRatio = orientation == Orientation.portrait
   ? PhotoboothAspectRatio.portrait
   : PhotoboothAspectRatio.landscape;
return Scaffold(
 body: _PhotoboothBackground(
   aspectRatio: aspectRatio,
   child: Camera(
     controller: _controller,
     placeholder: (_) => const SizedBox(),
     preview: (context, preview) => PhotoboothPreview(
       preview: preview,
       onSnapPressed: () => _onSnapPressed(aspectRatio: aspectRatio,),
     ),
     error: (context, error) => PhotoboothError(error: error),
   ),
 ),
);

通过拖放增加贴纸

I/O 照相亭的一大重要体验在于与您最喜爱的 Google 吉祥物合影并增加道具。您可能在照片中拖放吉祥物和道具,以及调整大小和旋转,直到取得您喜爱的图像。您也会发现,在将吉祥物增加到屏幕上时,您能够拖动它们并调整其大小。吉祥物们还是有动画成果的——这种成果由 sprite sheet 来实现。

for (final character in state.characters)
 DraggableResizable(   
   canTransform: character.id == state.selectedAssetId,
   onUpdate: (update) {context.read<PhotoboothBloc>().add(
       PhotoCharacterDragged(
         character: character, 
         update: update,
       ),
     );
   },
   child: _AnimatedCharacter(name: character.asset.name),
 ),

为调整对象的大小,咱们创立了可拖动、可调整大小且能够包容其余 Flutter widget 的 widget,在本例中,即为吉祥物和道具。该 widget 会应用 LayoutBuilder,依据窗口的约束条件来解决 widget 的缩放。在外部,咱们应用 GestureDetector 以挂接到 onScaleStart、onScaleUpdate 和 onScaleEnd 事件。这些回调提供了必要的手势详细信息,以反映用户对吉祥物和道具的操作。

通过多个 GestureDetector 回馈的数据,Transform widget 和 4D 矩阵变换即可依据用户所做的各种手势解决缩放,以及旋转吉祥物和道具。

Transform(
 alignment: Alignment.center,
 transform: Matrix4.identity()
   ..scale(scale)
   ..rotateZ(angle),
 child: _DraggablePoint(...),
)

最初,咱们创立了独自的 package 来确定您的设施是否反对触摸输出。可拖动、可调整大小的 widget 会依据触摸性能做出相应的调整。在具备触摸输出性能的设施上,您并不能看到调整大小的锚点和旋转图标,因为您能够通过双指张合和平移手势来间接操纵图像;而在不反对触摸输出的设施 (例如您的桌面设施) 上,咱们则增加了锚点和旋转图标,以适应单击和拖动操作。

针对 Web 优化 Flutter

应用 Flutter 针对 Web 进行开发

这是咱们应用 Flutter 构建的首批纯 Web 我的项目之一,其与挪动利用具备不同的特色。

咱们须要确保该利用对任何设施上的任何浏览器都具备 响应性和自适应性。也就是说,咱们必须确保 I/O 照相亭能够依据浏览器大小进行缩放,并且可能解决挪动设施和 Web 端的输出。咱们通过以下几种形式做到了这一点:

  • 响应式调整大小: 用户可能随便调整浏览器的大小,并且界面能做出响应。如果您的浏览器窗口为纵向,则相机会从 4:3 的横向视图翻转为 3:4 的纵向视图。
  • 响应式设计: 针对桌面浏览器,咱们设计为在右侧显示 Dash、Android Jetpack、Dino 和 Sparky,而对于挪动设施,这些因素则会显示在顶部。咱们针对桌面设施,在摄像头右侧设计应用了抽屉式导航栏,而对于挪动设施,则应用了 BottomSheet 类。
  • 自适应输出: 如果您应用桌面设施拜访 I/O 照相亭,则鼠标点击操作将被视为输出,如果您应用的是平板电脑或手机,则应用触摸输出。在调整贴纸大小并将其搁置在照片中时,这一点尤其重要。挪动设施反对双指张合和平移手势,桌面设施反对点击和拖动操作。

可扩大架构

咱们还为此利用构建了可扩大的挪动利用。咱们的 I/O 照相亭在创立之初就具备巩固的根底,包含良好的空安全性、国际化,以及从第一次提交开始就做到的 100% 单元和 widget 测试覆盖率。咱们应用 flutter_bloc 进行状态治理,因为它反对咱们轻松测试业务逻辑,并察看利用中的所有状态变动。这对于生成开发者日志和确保可追溯性特地有用,因为咱们能够精确地察看到从一个状态到另一个状态的变动,并更快地隔离问题。

咱们还实现了由性能驱动的繁多代码库构造。例如,贴纸、分享和实时相机预览,均在各自的文件夹中失去实现,其中每个文件夹蕴含其各自的界面组件和业务逻辑。这些性能也会用到内部依赖,例如位于 package 子目录中的相机插件。利用这种架构,咱们的团队可能在互不烦扰的状况下并行处理多个性能,最大限度地缩小合并抵触,并无效地重用代码。例如,界面组件库是名为 photobooth_ui 的独自 package,相机插件也是独自的。

通过将组件分成独立的 package,咱们能够提取未与此特定我的项目绑定的各个组件,并将其开源。与 Material 和 Cupertino 组件库相似,咱们甚至能够将界面组件库 package 做开源解决,以供 Flutter 社区应用。

Firebase + Flutter = 完满组合

Firebase Auth、存储、托管等

照相亭利用 Firebase 生态系统进行各种后端集成。firebase_auth package 反对用户在利用启动后立刻匿名登录。每个会话都应用 Firebase Auth 创立具备惟一 ID 的匿名用户。

当您来到共享页面时,此设置即会开始发挥作用。您能够下载照片以保留为个人头像,也能够间接将其分享到社交媒体。如果您下载照片,则该照片将存储在您的本地设施上。如果您分享照片,咱们会应用 firebase_storage package 将照片存储在 Firebase 中,以便稍后检索并生成帖子通过社交媒体公布。

咱们在 Firebase 的存储分区上定义了 Firebase 平安规定,确保照片在创立后不可变。这能够避免其余用户批改或删除存储分区中的照片。此外,咱们应用 Google Cloud 提供的 对象生命周期治理,定义了一个删除 30 天前所有对象的规定,但您能够依照利用中列出的阐明申请尽快删除您的照片。

此利用还应用 Firebase Hosting 疾速平安地进行托管。咱们能够借助 action-hosting-deploy GitHub Action,依据指标分支,将利用主动部署到 Firebase Hosting。当咱们将变更合并到主分支时,该操作会触发一个工作流,用于构建利用的特定开发版本,并将其部署到 Firebase Hosting。同样,当咱们将变更合并到公布分支时,该操作也会触发部署生产版本。通过联合应用 GitHub Action 与 Firebase Hosting,咱们的团队可能疾速迭代,并始终失去最新版本的预览。

最初,咱们应用 Firebase 性能监测 来监控次要的 Web 性能指标。

应用 Cloud Functions 进行社交

在生成您的社交帖子之前,咱们首先会确保照片内容是像素级完满的。最终图像蕴含丑陋的边框,以出现 I/O 照相亭特色,并按 4:3 或 3:4 的宽高比进行裁剪,以便在社交帖子上出现杰出的成果。

咱们应用 OffscreenCanvas API 或 CanvasElement 来合成原始照片、吉祥物和道具的图层,并生成您能够下载的单个图像。这个解决步骤由 image_compositor package 负责执行。

而后,咱们利用 Firebase 弱小的 Cloud Functions,来将照片分享到社交媒体。当您点击分享按钮时,零碎会带您返回新标签页,并在所选的社交平台上主动生成待发布的帖子。该帖子还蕴含一个链接,连贯到咱们编写的 Cloud Functions。浏览器在剖析网址时,会检测 Cloud Functions 生成的动静元数据,并据此在您的社交帖子中显示照片的精美预览,以及一个指向分享页面的链接,您的粉丝们能够在该页面上查看照片,并导航回 I/O 照相亭利用,以获取他们本人的照片。

function renderSharePage(imageFileName: string, baseUrl: string): string {const context = Object.assign({}, BaseHTMLContext, {
   appUrl: baseUrl,
   shareUrl: `${baseUrl}/share/${imageFileName}`,
   shareImageUrl: bucketPathForFile(`${UPLOAD_PATH}/${imageFileName}`),
 });
 return renderTemplate(shareTmpl, context);
}

成品如下所示:

无关如何在 Flutter 我的项目中应用 Firebase 的更多信息,请查看 此 Codelab。

最终成绩

本我的项目具体地示范了如何针对 Web 来构建利用的办法。令咱们感到惊喜的是,与应用 Flutter 构建挪动利用的体验相比,这个 Web 利用的构建工作流与之十分类似。咱们必须思考窗口大小、自适应、触摸与鼠标输出、图像加载工夫、浏览器兼容性等元素,以及在构建 Web 利用时所必须思考的其余所有因素。然而,咱们依然能够应用雷同的模式、架构和编码标准来编写 Flutter 代码,这让咱们在构建 Web 利用时感到十分自在。Flutter package 提供的工具和一直倒退的生态系统,包含 Firebase 工具套件,帮忙咱们实现了 I/O 照相亭。

△ 打造 I/O 照相亭的 Very Good Ventures 团队

咱们曾经凋谢了所有源代码,欢送大家返回 GitHub 查看 photo_booth 我的项目,也别忘了多多拍照秀进去哦!

退出移动版