很少有文章,介绍如何将大型应用,移植到Flutter。而本文的作者——一位来自澳洲的Native iOS & Flutter的开发者,尝试这样做了,结果让他十分惊讶。到底是什么情况?一起来看文章吧!
将 75000 行原生 iOS 应用程序移植到 Flutter 后,结果太惊讶!
澳大利亚有一个名为Easy Diet Diary的原生iOS应用程序。
该应用:
已被下载120万次;
用Objective-C和Swift编写,后端是Amazon AWS;
代码统计工具CLOC,报告该应用包含75,000行代码。
我在这家小公司工作了很长一段时间,他们的任务列表上一直有个安卓版本,但我们一直没有开发,因为:
支持两个代码库需要太多精力且难以管理。
跨平台开发的主要选择Xamarin和React Native都有重要缺陷(这是另一个故事)。
最后,我们选择进入Flutter的世界!
Flutter的速度很快,并且可以保证用户界面体验,而且一切都与原生应用没有区别(特别是有了2018年9月加入的iOS小窗体之后)。
以下是本文的主干:
代码行数与开发速度
架构
社区
性能
语言
还缺少什么?
结论
第一阶段我预估需要6个人月。但是,项目进度居然领先了!对我来说,这简直太不可思议了。
为什么?
Flutter的布局并不是像iOS那样,使用的不是Storyboard,或者像安卓那样使用XML,而是在代码中将窗体组合成窗体树,即可创建应用程序的用户界面。这对我来说听起来有点可怕,但是我做了尝试,虽然花了一些时间来习惯,但不久我就可以灵活地使用这些窗体树了。
然后,大约第一个阶段进展到第三个月的时候,发生了一件很奇怪的事情。随着我越来越熟练、越来越快地移植功能,项目中的代码行却开始迅速减少了。这很奇怪,因为我移植的业务逻辑数量非常庞大,这些逻辑在代码数量上的比例几乎是一比一。
事情的真相是,我可以通过创建类和编写函数来重用用户界面部分的代码,这比使用原生iOS更容易。通常,我可以利用几个额外的参数,简单地重构用户界面的窗体就可以重用它们。如果这样不行,我还可以简单地在现有的窗体周围再包上另一个窗体,就可以实现需要的行为了。这简直太赞了!
最终我预计代码行数将少于30,000(而原生iOS版本为75,000行)。当然,原生iOS版本包含一些虽然当时开发了最终却被取代或未被使用的代码。我估计未使用的代码占15,000行。换句话说,需要移植的代码量为60,000行。
因此,总的来说,Flutter的代码量只有原生iOS原有代码的一半!
此外,Flutter项目不包含任何Storyboard XML。Storyboard中有很多XML。原生iOS项目包含:
15个Storyboard;
47个Nib文件;
92个View Controllers。
我与Storyboard斗争了很多年,一直在尝试遵循最佳实践,最后感觉在Flutter中构建用户界面非常自由而且速度很快。
以前我没有意识到自己的大部分时间都用在了编写与用户界面相关的代码,还要忍受大量的Storyboard和自动布局限制。
我无数次听人说应该将用户界面布局与代码分开,我非常同意,但就我的Flutter经验而言,这一点未必是真的。而且关于用户界面分离的这种概念也不仅仅是原生iOS的最佳实践。
我记得自己在微软WPF中与XML作斗争,也见过Quora上有人问《为什么安卓使用XML来定义用户界面而不仅仅是Java代码?》
Storyboard是一种自上至下的布局方式(适用于桌面应用程序),而窗体采用自下而上的方法,在构建移动应用程序时,窗体可以极大地简化编程。我听说这种方式类似于React Native和CSS flex-boxes的布局方式。Wm Leler在Hacker Noon上发表的文章《Flutter带来了哪些创新》很好地解释了这个问题。
以下方式对于利用窗体构建用户界面很有帮助性:
大多数情况下,Flutter中支持状态的“热重载”功能可以在几秒钟内,将代码更改无缝地整合到正在运行的应用程序中;
Android Studio的快捷键Alt + Enter可以插入或删除用户界面的窗体(行、列或容器)。
我觉得VS Code中应该有类似的东西。这似乎微不足道,但我觉得这个快捷键十分有用。使用热重载和Alt + Enter,我可以在几分钟内做好一个画面,甚至可以在用户界面上做实验。
将debugPaintSizeEnabled设置为true。这会在所有用户界面的窗体周围显示鲜艳的边框。实际上这个功能用得并不如想象得多,但是当布局出现问题时该功能确实非常有用。
我在选择架构时做了如下几件事:
看了几遍Brian Egan的这个演讲《保持简单、有状态:Flutter应用的架构》
读了几遍Eric Windmill的这篇文章《有效地使用Flutter的继承窗体》
最终,我按照Eric的建议使用了一种名为Lifting State Up的架构模式,这是Redux的第一步,看起来非常诱人。然而,Redux对我来说遥不可及。由于开发时间紧迫,学习和尝试Redux似乎太令人生畏了。
一路走来,我从Stack Overflowers上的人解答的关于架构的问题中得到了许多帮助。这让我想到了社区。
有关Flutter的开源社区非常多元化,并且非常乐于助人,让我看到了人类的希望(特别是在这些疯狂的时期)。
举个例子,当时我正在使用Romain Rastel编写的flutter_slidable软件包,而且我还提了一个改善建议,仅仅48小时之内他就实现了一个比我想象的更好的古玩解决方案......类似这样的事情比比皆是。
我唯一遗憾的是,我一直忙于移植工作,与我收到的帮助相比,我给予别人的远远不够。
总的来说,参加了内部测试的用户对Flutter的“活泼”非常满意。在同一台iOS设备上,同时并排运行iOS应用与Flutter应用时,并没有看到性能明显下降。
我们的应用没有很多需要大量图形的任务,但有一个功能需要从数百个JSON文件读取数据,然后对该数据进行一系列浮点计算,在该功能中Flutter的应用明显更快。
我没有花时间去研究导致差异的究竟是是读取文件的功能、还是JSON解析或者是日期处理等等,因此我无法做出类别上的判断,但我感觉这无疑是Flutter桂冠上的一颗明珠。
很期待看到其他人能给出怎样的性能测试数据。
Flutter使用Dart,这种语言已经在Google之外萎靡不振,直到最近才随着Flutter再次兴起。
然而,这门语言很成熟且易于学习。幸运的是,我在加入Dart队伍时,拥有更强大的类型功能的Dart 2.0刚刚出现,所以无需再在代码中不断地敲“new”关键字了。
Dart可能没有Swift和Kotlin所拥有的Null和非Null类型,但我很喜欢它的简单性。 例如:
以下划线开头的函数为私有;
自动格式化意味着不必再头疼我的习惯问题——我喜欢在末尾加上逗号将参数分行;
包管理很简单。
还有很多很多。我喜欢这些功能,也许其他人可能不喜欢。对我而言,最重要的是我可以更快地(用奇怪但很愉悦的方式)实现功能。
没有太多缺少的东西。在原生iOS应用中:
我在XCode中使用了很多Targets,并结合一堆#define创建bundle ID不同的各种版本的应用。我不知道在Flutter中怎么处理这个问题;
我在iOS中使用了一个很好的第三方日志框架,叫做CocoaLumberjack。还没找到能代替它的东西;
我使用UIKit框架中的WKWebView来加载并渲染本地的HTML文件(如使用条款、隐私政策等)。我在Flutter中没找到正确的包来实现这个功能。最后只能使用一个包HTML2MD将HTML转换成Markdown再使用另一个包flutter_markdown来渲染;
我使用AVFoundation框架,在单一的全屏视图中实现条码扫描、QR二维码扫描和拍照。在Flutter中我还不能如此细致地进行控制,虽然它的Camera包很适合拍照,另一个由facundomedica编写的包fast_qr_reader_view很适合扫描条码。在安卓设备上,这个包与Firebase的ML Kit配合从Camera包中获取实时图像。
读到这里,你肯定不会惊讶为什么我如此赞美Flutter。到目前为止:
Flutter的用户界面几乎与原生安卓和原生iOS的用户界面没有区别;
得益于Flutter用户界面的构建方式,利用Flutter制作新功能比原生代码更快;
测试没有收到有关性能劣化的报告;
iOS版和安卓版之间的代码共享目前达到了90%以上。我还没有集成苹果的HealthKit或Google Fit,但我觉得共享率应该不会下降太多。
尽管Flutter还不成熟(2018年5月才发布正式版),但对于我来说它已经足够强壮了。
我遇到的唯一问题就是Dart同步读取目录的函数List Sync在最近的一次发布中出现了崩溃。我将它加到了Flutter在Github上的代码库中,然后改用异步版本。