如果你是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。
代码语言:javascript复制@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组成,其中包含电影图像的堆叠布局,渐变,气泡和文本图层。 能够以模块化的方式表达布局使得创建这种相当复杂的布局变得非常简单。 下面是实现:
代码语言:javascript复制@override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: primary,
body: new CustomScrollView(
slivers: <Widget>[
_buildAppBar(widget._mediaItem),
_buildContentSection(widget._mediaItem),
],
)
);
}
在构建布局时,我将布局的各个部分模块化为变量,方法或其他小部件。 例如,图像顶部的文字气泡只是另一个小部件,它将文本和背景颜色作为参数。 创建一个自定义视图就像这样简单:
代码语言:javascript复制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:
代码语言:javascript复制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:
代码语言:javascript复制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一样?
代码语言:javascript复制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可能会陷入困境,但它也有其特殊之处。
最后
如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。