我的 Flutter 之旅

在 20 年底的时候我第一次接触了 Flutter,当时正赶上谷歌在 B 站开设账号大肆布道,中文社区掀起了一阵热潮,遂跟着《Beginning Flutter》 和 《Flutter in Action》简单入了门 —— 当时还基本没有中文书籍,而现在这两本书都有翻译了,跟着这两本书的例子,认识了基本的组件、交互和滚动、弹窗和表单使用,也用自己的短链接和物品管理后端做支持,搭建了一套增删改查的客户端。

和基于 Scala 实现的后端中较容易理解的请求响应模型不同,在没有良好的 GUI 心理模型(比如 Web 前端)和 Native 开发经验的情况下,Flutter 的入门成本不可谓不高,且不说 iOS 和 Android 开发工具链搭建、依赖下载和开发环境准备的成本,Flutter 所使用的 Dart 开发语言也不是什么简洁易用的主 —— Dart 代码像 Java 一样啰嗦,且由于缺乏反射和元编程能力导致其并不灵活,这意味着 Flutter 需要开发者拥有对编程风格的品味和对面向对象精辟的理解才能够避免冗余并建立弹性的、可理解的和可扩展的抽象,以最佳限度的复用代码。

在我初次接触 Flutter 的时候,我还没有读过 SICP,没写过像样的 MVCC 架构的 UI 逻辑,因此糊出来的 Flutter 应用充斥着嵌套的括号,抽象程度差不说,代码可读性几乎为 0,并且莫名其妙的会因为组件渲染状态改变而请求多次 HTTP API。介于面对着这样的压力,终于我开发 App 的热情消失殆尽,正可谓三分热度,而写的那个客户端,就单纯的放在了 Gitee 上,在每隔 7 天就要重新验证一次开发者授权的 iPhone 上尘封了许久,也许是在哪次清理手机内存的时候不小心删掉后,终于不见了踪影。

但是回过头来看当初这个 App,功能上其实并不简陋,不论是可拖拽排序的列表页还是滑动删除和提示的交互,包括自动搜索补全、桌面短链接直接打开页面、调用相机插入并压缩图片、使用多个维度:上下滑动、左右滑动、点击、双击、长按/重按和简洁的界面完成复杂交互上,其实都有模有样,但却不论怎么也提不起来兴趣了。

时隔一年有半,在这个过程中我逐渐将精力移动到了 Web 平台,包括前端和后端。我读了一些关于 Clojure 和 ClojureScript 的书和文档,基于 Clojure/Script 技术栈搭建了自己的应用,前后端配合起来得心应手,开发效率快,且在桌面浏览器能很好地满足我的需求,这个 Clojure/Script 代码库前后堆砌了两万多行代码。好景不长,这套系统很快便遇到了问题:Web 的缺陷也正如同 JVM 的缺陷一样,其不能够很好的利用宿主平台的接口,比如应用图标右键、桌面 Widget 插件,对于相机、GPS 和位置传感器的直接使用,后台消息推送等等。而一个应用,最重要的就是能够让用户乐于使用,这些所谓的“辅助功能”其实在人机交互的情感分析中占据了极其重要的地位。

我总是想不起来写日记或者去做一件事,是因为 Web 界面并不立即可得,并且在上面写日记的体验很糟糕 —— 哪怕我先后实现了精美的界面、Markdown 支持和实时预览、图床支持和拖拽插入图片,在自己不是有强烈的欲望写日记的时候,也不会去碰它。而正相反,在我还在使用 DayOne 的时候,桌面小插件和极其顺手的写作体验能让我花费更少的时间去试图做一件事,哪怕打开一个 Web 界面也并不复杂,但相比较瞟一眼经常看的手机,从快捷菜单中滑动点击就打开相机拍照并记录文字还是逊色很多的。这种潜移默化的心理暗示会滴水石穿的影响对一件事的坚持程度,最终产生十万八千里的结果差异。

事情的转机出现在我翻阅 Lisp 山脉后更详细加入到 Clojure 社区后。基于 Clojure/Script 这一 Lisp 魔法的开发体验着实非常令人愉悦,包含单元测试、API 文档和结构良好的带有详细注释的代码结构的集成了 CI/CD 的 Clojure/Script 应用的生产效率非常之高,而我重拾 Flutter,也正是在看到了社区即将发布一个叫做 ClojureDart 的 Port,这帮人试图将 Clojure 编译到 Dart,以支持 Flutter 开发,它们已经在 App Store 上线了 Roam Research 这个 App,看起来非常的吸引人。一门表达能力丰富的语言,同时支持三大主流的跨平台应用,覆盖前端、后端和客户端开发,是多么美妙的事情,只要愿意,一个 Clojure 程序员甚至可以使用同一套业务逻辑在桌面基于 Election、JavaFx 和 Flutter 三门技术栈开发不同架构的 GUI 程序,并利用其宿主各自庞大的生态系统和工具 —— 从来没有语言做到过这件事,虽然大部分语言都试图这么做,尤其是每个野心勃勃的试图干掉 Java 的 JVM 方言一样,尤其是 Kotlin。

Clojude to Dart

Clojure to JavaScript

Clojure to Java Bytecode

二二年的五一遇上奥密克戎,无处可去,所以索性重拾了 Flutter,跟着 B 站“王叔不秃”的系列视频深入了解了布局、Key、异步、动画、列表和 Sliver,然后买了他写的那本《Flutter 组件详解与实战》的书,把一些常见和不常见的组件都混了个脸熟,又温习并深入思考了状态管理的东西:Provider 和 GetX,整理了沉寂多年的笔记,算真正的入了 Flutter 的门。这次“二进宫”后,已经能够比较熟练的利用裁剪、动画计时器、渐变过渡模拟健康码跑马灯的效果,能够使用一些共享的插画制作出来还算可以的简洁、一致的界面了。我把自己之前使用 Web 写的大屏(微软待办、HCM 系统、习惯养成系统)迁移到了 Flutter 上,看起来还不错,准备把整个日记系统也整到上面去 —— 这样就能解决每天通知写日记、自动从相机拍摄的照片中添加文字生成日记,同样的,有了 sqflite,可以很方便的在本地浏览多篇日记而不同每次都请求 HTTP API 了。

虽然 pub.dev 已经有了大量的库和插件,但 Flutter 的生态相比较 JS 还差不少距离。此外,相比较原生,Flutter 在降低了一部分移动应用开发成本的同时,也相应的损失了一些体验。在 Release 模式下体验一下 Flutter 的动画,对比一下 iOS 的原生动画,尤其是大量消耗内存的页面整页向左滑动切换界面,打开 Drawer 的第一次过渡 —— 差距是明显的,这还是 Flutter 针对性的将 Drawer 层合并布局并且 Release 做了 AOT 后的效果,当然,这也有 iOS 的设计理念和渲染优先级问题。但不管怎样,Flutter 的水平将处于并且长期处于原生和基于 JS 的移动端解决方案之间(更靠近原生)。

而这样的性能表现,也是在舍弃了基于 DOM 和 CSS 低效的多次布局,放弃了动态灵活的 JavaScript 语言而选择一门静态可 AOT 语言之后取得的结果。而真正 Flutter 所令人诟病的反射和热更新支持,则是 Web 灵活性的根本:应用每次打开基本都是动态的从服务器下载到客户端执行。在寡头 App 垄断的当下,引诱用户下载并且安装一款新的 App 的成本不容小觑。至于为什么到现在为止也很少有大型纯 Flutter 应用,反而都是一些大公司的遗留项目的 Flutter 混合开发的原因,则不单单是 Flutter 布道者口中所说的遗留问题,我认为对于现有项目使用 Flutter 快速迭代是对大公司有利的做法,因为他们可以在自己的巨无霸 App 中灵活的选择使用原生、Flutter 还是 WebView 以最大限度的满足业务在灵活性和性能之间的权衡,而对于大公司新项目而言,纯 Flutter 应用面临着验证性业务快速迭代的困难,而且不能通过接入 WebView 实现灵活性更新,因此 Flutter 总会面临着 Native + WebView 技术方案的挑战。

因此从需求的角度来说,Flutter 虽然有充足的竞争力,但却并不是开发 GUI 的必选之项,它足够好,足够高效,但还不足以扼杀对手,覆盖足够多的使用需求。

Ps. 写在一年半之后:Flutter 在过去的一两年中进步颇多,Dart 也进化了不少,其中最大的更新莫过于 Flutter 废弃 Skia 而采用 Impeller 渲染引擎,其在 iOS 动画流畅度得到了很大提升。此外 Flutter 对于原生能力的支持也在扩大,比如 iOS Home Widget 等,本机交互接口易用性增强,进一步压缩了原生应用的使用场景。最后,Flutter Web 引入了 带有 GC 的 WASM,在 Firefox 120x 和 Chrome 100x 以上表现很不错,生成的 JS 文件的体积进一步缩小,提升了 Web Port 的可用性,实际上,不论是桌面还是移动端 Web 平台上,Flutter 在很多方面的效率甚至已经超过了 DOM,比如长列表滚动。总的来说,不论是“全栈 Flutter” 还是自定义上层接口,只复用底层渲染管道的第三方魔改,Flutter 的未来都值得期待。