程序员踩坑之旅:将 75000 行 iOS 原生代码迁移到 Flutter!

Gary Hunter· 2019-09-12
本文来自 CSDN ,作者 Gary Hunter

人们普遍认为,如果想构建一个良好的移动应用,则必须建立iOS和Android两个版本。与此同时大多数企业想要的是:只实现一次业务逻辑,并快速打包成具有原生体验感的用户界面。

image.png
作者 | Gary Hunter

译者 | 弯月,责编 | 郭芮

出品 | CSDN(ID:CSDNnews)

以下为译文:

直到去年,我们公司的主要应用Easy Diet Diary还只有iOS原生版本。这是一款澳大利亚的通用减肥跟踪应用,另外还有一个面向研究以及不幸患有肾病用户的特殊版本。该应用程序的大致状况如下:

  • 拥有75,000行Objective C和Swift代码;

  • 亚马逊AWS后端:DynamoDB、Postgres和S3;

  • 每天都有22,000名用户和125万次下载。

后来,Flutter问世了(Beta版于2018年4月2日问世)。Flutter能够为我们提供足够多的功能:跨平台、良好的性能、快速实现、原生体验、开源,我们只需构建Flutter一个版本,就可以同时服务于iOS和Android。
六个月后,我发布了Google Open Beta,却没有使用原生代码。
本文是在Open Beta的基础上:

  • 通过谷歌应用商店发布Android版本;

  • 直接替换掉苹果的应用商店中原有的iOS原生应用。

本文的主要内容包括:

  • 代码行数与开发速度

  • Google Open Beta

  • 架构

  • 后端服务(亚马逊AWS)

  • 性能

  • 原生代码

  • 跨平台设计

  • 风格与方案

  • 总结

  • 后记


代码行数与开发速度

在导入代码的时候,我真实地感受到了声明式编程的效率有多么高,而且摆脱基于XML的故事板的束缚,能够重用界面代码是多么方便。好吧,随着Jetpack Compose和SwiftUI的推出,似乎这些也习以为常了。
最终我获得了35,000行Dart代码。此外,还有3000行Objective-C/Swift代码负责处理HealthKit等iOS特定的逻辑,以及500行Java图像处理代码。
导入完成后,Flutter应用的代码行数只有iOS原生应用的一半。

Google Open Beta

我花了大量时间通过苹果的TestFlight流程来操纵应用,却发现将一个不断发展的应用交到广大用户手中是多么艰辛。而且我觉得短期内这种状况还无法得到改善,因为苹果认为其审核流程是确保应用符合某些标准的方式,他们并没有恶意刁难。对于能力很强的热心开发人员来说,可能会觉得很受打击。
相比之下,广大用户可以利用Google Open Beta的流程,在应用商店中像搜索其他应用一样搜索测试版的应用,而且还可以无缝地加入测试版程序,使用这些应用并提供(有限的)反馈。如果你对Open Beta版本感到满意,那么就可以升级到正式版本。如果应用的使用合理,那么用户很快就会接纳并提供建设性的反馈。至少我个人的经历如此——这是一种良好的开发方式。
随着我添加功能和修复错误,Easy Diet Diary累积了10,000个beta用户。于是,3月份我发布了1.0 Android版本。

image.png

架构
去年刚开始的时候,我还不熟悉声明式UI编程以及随之而来的状态管理。对我来说,依赖于异步流的Redux和BLoC学习曲线十分陡峭,最终我利用InheritedWidgets在部件树中实现了状态同步。我想方设法将业务逻辑与表示逻辑分开,但并没有使用状态管理框架来强制两者的分离。
从那以后,有关状态管理的方法和热烈讨论与日俱增。将Flutter中状态管理的开源演变与SwiftUI的响应式编程框架(另一个更大的团队正在秘密开展工作)的开发进行对比是一件有趣的事情,这颇像史蒂夫·乔布斯的风格。Matt Gallagher对苹果的Combine框架进行了一些有趣的分析。
在2019年的Google I/O大会上,为了减少开发人员对于状态管理的恐惧心态(我认为部分原因是如此),并抑制越来越多的InheritedWidget封装库的出现,Flutter团队宣传了由Remi Rousselet开发的provider小部件,详细说明请点击这里(https://www.didierboelens.com/2019/07/provider---points-of-interest---points-to-care-about/)。Provider给予了我很大帮助。但我仍然没有使用更为正式的状态管理方法,比如BLoC或MobX(两者都可以与Provider一起使用),因为需要修改大量的代码。
在阅读了Didier Boelens的博客后,加速了我对状态管理的理解。虽然他是BLoC的粉丝,但他善于用客观的方式解释这些技术。
后端服务(亚马逊AWS)
除了Crashlytics和ML Kit之外,所有Easy Diet Diary的云服务都在Amazon AWS上。
不幸的是,到目前为止,我们还没有适用于AWS的官方Flutter SDK,而与AWS相关的插件也非常少。
对我来说,这倒不是一个大问题,因为在我们的移动应用与所有的AWS服务之间,还有我们自己的服务器……除了S3(云文件存储)。我们的服务器负责验证和同步存储在DynamoDB中的用户日志。
iOS原生应用可以绕过我们的服务器,通过AWS S3 SDK直接上传和下载照片。如果想切换到Flutter,我只能使用到预签名的S3 URL(由AWS通过我们的服务器提供)。这种方法的效果很好。
虽然我感觉移植到Flutter并不会太痛苦,但我认为Flutter如果有AWS SDK插件就会更受欢迎(不一定是正式的插件)。如果说云服务商的收入能大致代表移动应用对于云服务的使用状况,那么AWS就是目前的市场领导者。

image.png

我希望能够通过Dart来试验大量的AWS服务。我确信Azure也是如此,尽管我没有使用Azure。
性能
我在Easy Diet Diary上测试Flutter的方法大概是,在屏幕中间显示一个可移动的相机的视口,或者在读取数百个20KB左右的JSON文件后显示图表。
在比较iOS原生版的应用与Flutter版时,我们的测试人员(实际上我们只有一位主要的测试人员)并未在手机上看到明显的性能下降,除了iPhone 6(该应用在6S上运行良好)。iPhone 6于2014年发布,但市场上还有销售,我十几岁的儿子就有一部。我们在其他手机上测试的时候,发现了一些较慢的设备,包括三星Galaxy J5 Pro和摩托罗拉G5S Plus(移植时我的手机)。
刚开始的时候,iPhone 6有点卡,还有一些抖动,特别是移动相机的视口,但随着几个月后新版本的Flutter发布,这种现象逐渐减弱(但没有完全消失)。该应用在其他速度较慢的测试手机上运行良好。
题外话:目前我的个人手机是Pixel 3a,一个Android 10的中档手机。它可以顺畅地运行Easy Diet Diary,而且还具备了Android的开放性和灵活性,同时比我用过的其他Android手机更接近iPhone的体验。这款手机可以很好地适应我希望不断发展跨平台的想法,只是需要更多具备弹性的应用。
原生代码
在移植的过程中,我一直在极力避免原生代码。
然而,在图像处理、HealthKit集成和升级旧用户方面,我做得不够好。
对于HealthKit和升级旧版用户功能,我简单地沿用了iOS的原生Swift和Objective C代码。
对于图像处理,我编写了我的第一个Java代码,应用的用户可以在各种情况下拍照。现代手机拍摄的百万像素照片会占用大量空间,因此我在显示或上传之前进行了裁剪、缩放和压缩,这些处理都需要原生的iOS和Android代码。我找不到适合的插件(特别是能够正确保留EXIF元数据的插件),所以我利用开源的插件、StackOverflow上的解答以及旧的Objective-C代码拼凑了一个自己的解决方案。
按照这个说明(https://flutter.dev/docs/development/packages-and-plugins/developing-packages)编写插件包非常方便。在应用启动运行后,你可以通过配置启动原生代码,并设置断点等。当然,不支持热重载。
我原本希望将这些处理放到一个隔离区,这样它们可以自由地花时间处理和上传图像。但是,没想到我无法从隔离区调用插件代码,所以我只好在原生插件中分出来了一个线程。
跨平台设计
你可以利用Flutter控制应用的原生状态。因为我想为现有用户编写iOS原生应用的替代品,所以我希望这个Flutter应用非常接近原始版本。
首先,我建立了一个Material Design应用。为了让自己沉浸在这个过程中,我把自己原来的iPhone换成了一款中档的Android设备——摩托罗拉G5S Plus——我不想要太高端的东西。
然后,在通过Open Beta流程建立了该应用的管理后,我打算做一个iOS版。如果放到今天,我肯定不会这么做。刚开始时,我选择了一些有iOS风格的小部件。然后,逐步进行扩展,发展到如今已经可以构建同时具备Material Design和iOS风格的应用了,而且还可以根据需要在窗口小部件树中替换小部件。
我不打算在本文中详细介绍我建立跨平台应用的过程。简而言之:

  • 有些看似重大的差异其实很容易实现。例如,因为UI全是小部件树的代码,所以你只需去掉底部标签栏上的Android侧滑菜单,换成iOS的“更多”按钮。

  • 有些小差异非常繁琐。例如,利用相同的代码在两个平台上显示原生的对话框,但是最终还是会有非常细微的差别。也许这不是一个小差异?

在我完成这些工作后,才发现了这个文档:平台特定的行为和修改(https://flutter.dev/docs/resources/platform-adaptations)。多么希望一开始就能看到这个文档!
风格与方案
Flutter的设计旨在通过相同的代码构建可以在多个平台上运行的应用,但是如何在一个平台上通过相同的代码构建多个应用程序呢?
对我而言,我在其上花费的时间远远超出了预期。但最后,我也学习到了很多经验教训。
看起来很简单。Flutter有一个命令行开关(可以通过IDE设置),你可以将构建风格指定成Gradle产品风格或Xcode方案。
flutter build --flavor research

我希望复制原生Xcode项目中设置的内容。在Xcode中,你可以利用方案名称区分应用的版本。方案简单易用,在创建时,你只需告诉Xcode希望使用的配置和目标:

  • 这里的配置是一个普通文本文件,其扩展名为.xcconfig。你可以指定当前应用版本所特有的环境变量。例如,包标识符后缀等。

  • 这里的目标指的是所有构建的设置(可能有几百个)以及构建中包含的类、资源和自定义脚本的列表。每个新的Xcode项目都有一个目标,在Flutter中,它被称为“Runner”。

在原生的iOS应用中,不同版本的应用有不同的目标。大多数人都采用了这种方式。然而,如果你想让Flutter使用不同的目标,那么就要下一番苦功夫了(换句话说,Flutter的多目标几乎能用)。
最后,我得出了与Salvatore Giordano相同的结论,我使用了单个目标并完全依赖于配置。后来,我读到了Matt Thompson撰写的这篇好文(https://nshipster.com/xcconfig/),我就释然了。根据Mattt的说法,只有一个目标的配置文件可以带来以下好处:

  • 你不需要Xcode来管理它们。它们是纯文本,这意味着可以通过源代码控制系统管理他们,而且还可以使用任意编辑器进行修改。

  • 你可以使用它们来设置捆绑包的标识符、应用图标集(供Google服务信息文件使用)等。

此外,由于无法在iOS中使用不同的目标,所以我明白了导致iOS应用各个版本各异的原因,而且也让我感受到配置Android产品风格的简单性。

总结
构建Flutter应用的方式:

  • 它的底层平台有点像metal游戏引擎。

  • 它的小部件树结构支持“热重载”,允许你快速灵活地构建用户界面。

  • 它为Android和iOS提供了一整套原生风格的小部件,可以根据平台在小部件树中轻松替换。

  • 你可以利用它的开源代码库,根据需要调试问题并调整或增强小部件,或者只是尝试各种功能。

上述这些优点都非常棒。
Flutter需要改进的地方(根据我的经验):

  • 更好地支持非Google云服务。就我的情况而言,需要支持亚马逊AWS。

  • 感觉GitHub上有一些关于Flutter的问题更适合放在Stack Overflow上。也许人们认为,那些创造了bug的开发人员(包括我在内)能够更好地回答问题?虽然我不知道如何改善这种情况,但也许这称不上问题?

  • 用更简单、更明确的指令,更顺畅地部署到iOS和Android。创建用户界面的工作很有意思。与Cocoapods和Gradle结合使用,就可以部署到特定平台,也不是特别难。开发人员的专业知识往往会偏向某个平台(比如我更熟悉iOS),因此部署到其他平台可能很困难。所以越简单越好。

  • 更高效的图像显示。iOS原生应用使用的SDWebImage库效果很好。Flutter的cached_network_image库则不太理想。

一点心得:

  • 在使用了多年的TestFlight后,我非常喜欢Google应用商店的Open Betas,我们利用它来让公众测试不断完善的应用。

  • 学习Flutter很有趣。Emily Fortuna在这个视频中解释了Flutter的关键功能(https://www.youtube.com/watch?v=kn0EOS-ZiIc)。

  • Flutter团队非常平易近人,他们在努力打造一个友好而多样化的开源社区。

  • 上周,Ray Wenderlich(一个对我学习iOS不可或缺的网站)推出了Flutter部分!Flutter一定会得到推广。

  • 至于面向Web的Flutter,虽然我很欣赏Flutter和Dart在这方面的独特优势,但我希望Flutter团队不要太过于分散精力,并且可以继续专注于改善Flutter的移动跨平台功能。

后记
无论SwiftUI或Jetpack Compose多么诱人,大多数公司趋之若鹜的仍然是一个优秀的跨平台移动解决方案。至少,我的经历是这样告诉我的。此外,在构建能够替代一个较大的iOS原生应用之后,我个人的感觉是Flutter值得考虑。
欢迎大家在下面留言。
原文:https://medium.com/flutter-community/finished-porting-a-75-000-line-native-ios-app-to-flutter-b5c0bff93715
作者:Gary Hunter,iOS native & Flutter开发。
本文为 CSDN 翻译,转载请注明来源出处。