如果你是 Android 开发者,那么可能已经听说过 Flutter。 这是一个相对较新,用来开发跨平台原生应用的框架。 这不是第一个移动领域用于跨平台开发的框架,但它正在被谷歌使用,得益于谷歌的实力,让 Flutter 有一定的可信度。 尽管最初持有保留意见,但我决定尝试一下 - 结果 Flutter 在一周内彻底改变了我对移动开发的看法。 下面是我学到的东西。
“Hummingbird with a long beak flies through the air” by Randall Ruiz on Unsplash
在开始之前,先打一个预防针。 这篇文章演示用到的 app 相对简单,几乎没有业务逻辑。 示例很基础,但这是分享将原生 Android 应用移植到 Flutter 的最好例子。该示例没有任何架构,就是最纯粹的原生调用。
一年前,我在 Play Store 上架了第一款 Android 应用。 该应用的架构和编码都非常简单 ; 这是我的第一个大型开源项目,这个 app 见证了我的 Android 学习道路。之后在一家代理公司工作,接触到了不同的技术和架构,包括 Kotlin,Dagger,RxJava,MVP,MVVM,VIPER 等,这些对我的 Android 开发确实有帮助。
然而,在过去的几个月里,很想吐槽一下 Android 开发,特别是关于兼容性。关于每次调试的构建时间更是无力吐槽…(强烈推荐这篇文章,它会深入探讨更多细节),Kotlin 和 Databinding 的出现让问题有所改善,但仍然是杯水车薪。 Flutter 可以说出现的很及时。
几周前我开始使用 Flutter,那时还是 beta 版。通过官方文档和示例开始了 Flutter 的学习旅程(文档写的特别棒)。 很快,我开始理解 Flutter 背后的设计思想,并决定自己尝试一下,看看能否将 Flutter 投入使用。 一开始我在想用什么项目来练手,考虑后决定移植我的第一款 Android 应用到 Flutter。 这似乎是一个合适的选择,因为它可以让我以入门的姿态比较两种框架的优劣,同时不会过分关注应用程序架构。
我首先创建了网络请求,解析 JSON,并习惯了 Dart 的单线程并发模型(这可单独作为一个主题来讲)。 在接收到网络请求响应后,开始创建列表布局和列表元素。 Flutter 创建布局的只需要扩展各种 Widgets 并重载几个方法。 接下来我会比较 Flutter 和 Android 在构建这些功能时的差异。 让我们从在 Android 中构建此列表所需的步骤开始:
- 用 XML 创建 list-item 布局文件
- 创建一个适配器来绑定视图并设置数据
- 为列表创建布局(可能在 Activity 或 Fragment 中)
- 填充 Fragment/Activity 中的列表布局
- 在 Fragment / Activity 中创建适配器,布局管理器等的实例
- 在后台线程上从网络下载电影数据
- 回到主线程设置适配器中的项目
- 现在需要考虑保存和恢复列表状态等细节…
当然,这很繁琐。 构建这些功能其实是相当普通的任务,这是一个很寻常的用例, 你可能很想知道:是否有更好的方式来实现? 一种不太容易出错的方式,能否只涉及较少的样板代码,提高开发速度? 下面该 Flutter 入场了。
Flutter 吸收了移动开发领域多年来在应用程序开发,状态管理,应用程序架构等方面积累的经验,这也是为什么会与 React.js 如此相似的原因。用 Flutter 的方式来构建应用时正确的开始。 下看看如何在 Flutter 中实现上面的例子:
- 为电影项目创建一个无状态的 Widget(无状态,因为包含静态属性),接收一个 movie(例如 Dart 类)作为构造函数参数,并以声明方式描述布局,同时绑定电影的值(名称 ,发布日期等)到 Widget
- 为列表创建一个 Widget。
@override Widget build(BuildContext context) { return new FutureBuilder( future: widget.provider.loadMedia(widget.category), builder: (BuildContext context, AsyncSnapshot<List<MediaItem>> snapshot) { return !snapshot.hasData ? new Container( child: new CircularProgressIndicator(), ) : new ListView.builder( itemCount: snapshot.data.length, itemBuilder: (BuildContext context, int index) => new MovieListItem(snapshot.data[index]), ); } ); }
让我们看看这里发生了什么。 最重要的是,我们使用了 FutureBuilder(Flutter SDK 的一部分),它需要我们指定一个 Future(回调)和一个构建器函数。 构建器函数为我们提供了一个 BuildContext 和要返回的项目的索引。 使用这个,我们可以检索一个电影,给定 Future 的结果列表,快照,并创建一个 MovieListItem-Widget(在步骤 1 中创建),并将该电影作为构造函数参数。
然后,当第一次调用构建方法时,开始等待 Future 回调的返回结果。 一旦得到返回结果,构建器会再次被调用,我们可以用返回结果来构建我们的 UI。
这两个类与 API 调用结合起来会有以下结果:
这貌似太简单了…现在有没有感觉到用 Flutter 创建列表很容易,继续探索吧。
下一步我们尝试稍微复杂的布局。 该应用的电影详情有相当复杂的布局,包括约束布局和应用程序栏。 这样的布局展示能获得用户的青睐,如果 Flutter 想要在 Android 里有立足之地,那么需要能够提供更复杂的布局方式。 下面看看我是如何构建的:
该布局由 SliverAppBar 组成,其中包含电影图像的堆叠布局,渐变,气泡和文本图层。 能够以模块化的方式表达布局使得创建这种相当复杂的布局变得非常简单。 下面是实现:
@override Widget build(BuildContext context) { return new Scaffold( backgroundColor: primary, body: new CustomScrollView( slivers: <Widget>[ _buildAppBar(widget._mediaItem), _buildContentSection(widget._mediaItem), ], ) ); }
在构建布局时,我将布局的各个部分模块化为变量,方法或其他小部件。 例如,图像顶部的文字气泡只是另一个小部件,它将文本和背景颜色作为参数。 创建一个自定义视图就像这样简单:
import 'package:flutter/material.dart'; class TextBubble extends StatelessWidget { final String text; final Color backgroundColor; final Color textColor; TextBubble(this.text, {this.backgroundColor = const Color(0xFF424242), this.textColor = Colors.white}); @override Widget build(BuildContext context) { return new Container( decoration: new BoxDecoration( color: backgroundColor, shape: BoxShape.rectangle, borderRadius: new BorderRadius.circular(12.0)), child: new Padding( padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0), child: new Text( text, style: new TextStyle(color: textColor, fontSize: 12.0), ), ), ); } }
想象一下,在 Android 中构建一个像这样的自定义视图有多困难。不过,在 Flutter,这分分钟解决。能够将用户界面的一部分抽取到像 Widget 这样的自包含单元中,可以轻松地在应用程序中甚至跨不同应用程序重复使用这些小部件。这个应用中,布局的很多部分都在不同界面上重复使用,并让我告诉你:这真的很简单。前面太容易了,我决定扩展应用程序,合并电视节目。几个小时后也顺利完成了。该应用程序包含了电影和电视节目,并且开发过程中没有遇到任何困难。我通过构建用于加载和显示数据的泛型类来实现,这使得我可以重复使用电影和演出的每个布局。如果用 Android 实现相同的事情,我必须为电影和演出分别使用不同的 Activity。可以想象这让维护工作瞬间变得复杂,并且 Android 对于布局的共享处理方式不太灵活。
在 Flutter 体验结束时,我得出了一个非常直接和令人信服的结论:
我编写了更易维护的跨平台代码。 同时花费了更少的时间写了更少的代码。
现在不用再像 Fragment 一样去管理状态,这很繁琐也很容易出错。 不用再为一点点修改而重新构建应用,浪费时间。 不再有 XML 布局, 也不再有 findViewById。 不再有多余的样板代码 。
既然两个 app 的功能几乎一样,我就比较好奇两种不同语言实现的代码量。 那么应该如何进行对比?(免责声明:Flutter 版本中还没有实现持久化,原生代码写的也很乱)。 我们使用 Cloc 来进行代码的比较,为了简单起见,我们来看 Android 上的 Java 和 XML 文件,以及 Flutter 版本的 Dart 文件。
原生 Android 中的 Java:
Meta-Data for the native Android app http://cloc.sourceforge.net v 1.60 T=0.42 s (431.4 files/s, 37607.1 lines/s) ---------------------------------------------------------------------- Language files blank comment code ---------------------------------------------------------------------- Java 83 2405 512 8599 XML 96 478 28 3577 Bourne Again Shell 1 19 20 121 DOS Batch 1 24 2 64 IDL 1 2 0 15 ---------------------------------------------------------------------- SUM: 182 2928 562 12376
Flutter:
Meta-Date for the Flutter app http://cloc.sourceforge.net v 1.60 T=0.16 s (247.5 files/s, 14905.1 lines/s) ---------------------------------------------------------------------- Language files blank comment code ---------------------------------------------------------------------- Dart 31 263 39 1735 Bourne Again Shell 1 19 20 121 DOS Batch 1 24 2 64 XML 3 3 22 35 YAML 1 9 9 17 Objective C 2 4 1 16 C/C++ Header 1 2 0 4 ---------------------------------------------------------------------- SUM: 40 324 93 1992 ----------------------------------------------------------------------
先比较一下文件数量:
Android:179(.java 和.xml)
Flutter:31(.dart)
代码行数:
Android:12176
Flutter:1735
我期待的 Flutter 版本可能只有原生 Android 的一半代码量,但实际减少了 85%的代码量?完全超出预期。 但仔细想想又在意料之中:因为所有的布局,背景,图标等都需要用 XML 来指定,并且仍然需要使用 Java / Kotlin 代码连接到应用程序, 这里产生了大量的代码。 使用 Flutter 可以一次性完成上面的步骤并把值绑定到 UI 上。 现在无需处理 Android 中的数据绑定,比如设置监听器或处理生成的绑定代码。 在 Android 上构建这些基本的东西非常繁琐。 为什么要一次又一次地为 Fragment / Activity 参数,适配器,状态管理和恢复等类似的代码编写相同的代码?
通过 Flutter,只需专注于构建产品。
当然,这仅仅是 Flutter 的开始,因为它仍处于测试阶段,远没有 Android 成熟。 不过,相比之下,Android 似乎已经达到了极限,很快就可以使用 Flutter 中编写 Android 应用程序了。 还有一些事情需要解决,但总的来说,Flutter 的未来看起来很光明。目前 Android,VS Code 和 IntelliJ 都已经拥有支持 Flutter 的插件,并且还会有更多的工具会陆续产生。 这一切都让我相信,Flutter 不仅仅是另一个跨平台框架,而是更大的开始 - 应用程序开发新时代的开始。
而 Flutter 可能远远超出 Android 和 iOS 领域 ; 你应该有听说 Google 正在开发一个名为 Fuchsia 的新操作系统。 事实证明,Fuchsia 的用户界面正在使用 Flutter 构建。
当然,你可能会问自己:我现在必须学习一个完整的其他框架吗?刚学习了 Kotlin 并使用架构组件,现在一切都很好。为什么我们想要去了解 Flutter?但让我告诉你一点:在使用 Flutter 之后,你将开始理解目前 Android 开发存在的问题,并且很明显 Flutter 的设计更适合现代的,响应式的应用程序。
当开始使用 Android 的 Databinding 时,我认为这是革命性的,但它也感觉像是一个不完整的产品。使用 Databinding 处理布尔表达式,监听器和更复杂的布局相当繁琐,这让我意识到 Android 并不是为这样的工具设计的。Flutter 使用 Databinding 相同的思想,即将视图 / 小部件绑定到变量,而无需在 Java / Kotlin 中手动管理数据绑定,不用专门的绑定文件来桥接 XML 和 Java。这可以将以前至少有一个 XML 和 Java 文件的内容压缩到一个可重用的 Dart 类中。
我也可以争辩说 Android 上的布局文件本身并不做任何事情。 他们必须先布局,然后才可以设置值。 这也引出了状态管理问题,并提出了一个问题:当绑定的数据发生了变化应该怎么处理? 手动获取相应视图的引用并设置新值? 这种方法真的很容易出错,这样管理 View 的状态很差劲。 相反,我们应该使用状态来描述布局,每当状态发生变化时,框架会重新渲染视图。 这样,我们的应用程序状态就不会与 Views 显示的内容不同步。 而 Flutter 正是这样做的!
还有另外一个问题:你有没有问过为什么在 Android 上创建工具栏菜单非常复杂? 我们为什么要用 XML 来描述菜单项,这无法将任何业务逻辑绑定到 XML(这是菜单的全部目的),然后在 Activity / Fragment 的回调中进行过设置,然后再绑定真实回调到另一个回调上? 为什么不可以一次性设置好,就像 Flutter 一样?
class ToolbarDemo extends StatelessWidget { @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( actions: <Widget>[ new IconButton( icon: new Icon(Icons.star), onPressed: _handleClickFavorite ), new IconButton( icon: new Icon(Icons.add), onPressed: _handleClickAdd ) ], ), body: new MovieDetailScreen(), ); } _handleClickFavorite() {} _handleClickAdd() {} }
正如上面代码所示,我们将菜单项添加为 AppBar 的 Actions。 这就是全部 - 不需要再将图标导入成 XML 文件,不再需要重写回调。 只需要在 Widget 上加一些小的 Widgets 就可以了。
我可以继续下去,你可以思考一下:Android 开发目前存在的问题,然后考虑如何重新设计框架来解决这些问题。 这是一项艰巨的任务,但这样做会帮助你理解为什么 Flutter 会出现。 公平地说,有很多应用程序(截至目前),我仍然会使用 Kotlin 去编写 ; Android 可能会陷入困境,但它也有其特殊之处。
感谢覃云对本文的审校。
活动推荐:
2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。
评论