做者 | 稻子原文连接:https://mp.weixin.qq.com/s/m0...git
Flutter 在咱们团队的起步算是比较晚的,直到 Flutter 要出 1.0 版本前夕才开始实践。github
大概的时间线以下:面试
做为一个创新业务的团队,要作一门全新技术栈的技术储备面临如下几个问题:架构
这三个问题都是很是现实的问题,若是没有明确的路线规划盲目的引入 Flutter 的,踩坑过多最终会导入投入产出比过低而在业务上没法接受。异步
我把实践路线主要分一下四个阶段:ide
下面介绍在每一个阶段咱们作了哪些事以及得到的成果和经验。模块化
目标设定:提高人效 50% ~ 100%组件化
关键行动:布局
一样是手机开发,为何要分IOS和Android?若是分IOS&Android,那么,它必定是由于价格不一样。Ios手机5K以上,Android基本在500~5000。可是对于应用开发了说,一个应用,须要开发两套,一套IOS,一套Android,那么,能不能只开发一套,让他们运行在两个平台上呢?答案是确定的,那就是混合开发(Hybrid)Flutter计算。
须要的伙伴能够post
若是你正在寻找 Flutter 的学习资源,下面我整理了一些关于 Flutter的资料,须要的私信( Flutter)我分享给你。这份资料能够帮助新手开始 Flutter 的旅程,也能够帮已经了解过这方面的朋友更进一步。但愿能帮到大家。
有须要的朋友迎加入群聊:875911285(记得备注思否)到管理员处领取,或者 点击下面连接哦,有不对的地方也欢迎指出,一块儿交流共同进步。
若是你有其余须要的话,也能够在 GitHub 上查看,下面的资料也会陆续上传到Github
Flutter核心进阶学习资料
Flutter基础篇
Flutter项目实战
demo 验证
在技术储备阶段,主要是准备最小可验证的 demo,验证如下几点:
创建规范
没有规范,会增长后续人员的入门成本:
人员准备
团队分红两组,前后入坑 Flutter,主要作如下准备:
降级方案
虽然咱们是创新业务,但出于对线上敬畏之心,咱们依然准备了降级的方案,一旦 Flutter 上线以后影响到 App 的稳定性,能够随时降级。
因此咱们选择了既有的模块,将这些模块用 Flutter 从新开发一遍。同时也为后续的人效对比提供数据支撑。
代码量减小
仅供参考,咱们 Flutter 的代码量实践下来会比任何一端的代码量都少一些,相对于 iOS,咱们通常是纯代码布局,代码量减小更多。
更少的代码,必定程度上表示更少的 bug,更少的 bug 表示花在修复 bug 上的时间也减小了。
多端一致性
Flutter 渲染的多端一致性,让咱们在 UI 布局上所花费的时间更少了。固然早期的 Flutter SDK 在处理字体、光标等方面略有差别,甚至有 bug,但都不是很大的问题。
人效提高
仅供参考,毕竟每一个团队的状况不尽相同,业务复杂度也不尽相同。
这里给出咱们早期的三个数据的对比,19 年咱们下半年的时间基本上进入了纯 Flutter 开发的阶段,但 iOS 和 Android 两端仍是须要分别打包、测试、上线,这会必定程度上下降人效提高的百分比,因此咱们综合的人效提高会在 90% 左右。
业务价值
经过引入 Flutter,咱们在业务上能更快的进行迭代,使用 Flutter 开发的部分人效提高接近 90% 左右,由于咱们总归是有一些功能须要用原生进行开发的,这部分工做量很差作对比。
这达成了咱们最初引入 Flutter 设定的目标,提高了整个团队的人效,完美的支撑了业务的快速迭代。
在业务验证阶段,咱们达成了提高人效 90% 的目标以后,欠缺的持续集成须要被提上日程,最紧迫的两个事情就是 插件发布 和 编译产物发布。
做为一个业务团队,咱们依然没有太多精力投入到工程建设上,因此不少工程化相关的能力,最开始都是手工的方式进行的,大概能够分几个阶段:
手工发布
脚本发布
这个阶段主要是经过脚本实现 插件发布 和 编译产物发布 的半自动化,但依然没有集成到 App 发布的 CI 系统。
这个阶段也是在不断完善发布脚本,最终效果是根据 pubspec.yaml 文件的描述,自动发布有更新的插件,并最终发布编译产物。
一键发布
将现有的发布脚本集成到 App 发布的 CI 系统,效果就是一键打包,完全将这块活自动化。
架构建设方面,咱们须要解决的三个主要问题:
在解决这三个问题的过程当中,咱们大体经历了从 架构 1.0 到 架构 2.0,除了页面模块化基本保持不变,页面间通讯、页面栈管理从 架构 1.0 到 架构 2.0 的变化是很是大的。
页面状态管理 在咱们的业务上还不是一个主要问题,咱们也尝试过引入 bloc,但还未进行足够探索,因此这里不作展开。
模块化的定义,根据业务域划分不一样的业务模块,为了不与 WebComponent 的区别,不使用组件化这个名词。
如何划分模块这可能须要另一篇文章来讲明,简单来讲就是业务域的划分。要保持模块的内聚,每一个模块的初始化须要独立进行,要作到这点,咱们的方案是将全部模块挂载到模块树上,相似文件夹的树形结构。
页面模块化 1.0 主要提供如下能力:
挂载完成以后,初始化 root 模块,会将全部挂载在树上的模块都进行初始化。这个树形结构在叶子节点就是页面,页面的路径自然可做为页面的 url。
模块划分本质上是根据业务域对页面进行组织。无论是单一仓库仍是多仓库,均可以经过这种简单的树形结构来实现模块挂载和初始化。
模块间通讯,本质上主要是页面间通讯。
移动端不少模块化的方案,都会将模块间通讯做为主要能力进行建设,咱们在原生端也有一套这类方案,但在 Flutter 嵌入原生应用中,并不能简单复用这套方案,若是生搬硬套会带来不少的编码量,并非一个很轻量的解决方案。
页面间通讯的能力,须要重头开始建设,早期咱们抽象了一个状态同步的方案,开发一个插件 topic_center 专门用来给原生和 dart 进行状态同步。
topic_center 提供的能力:
topic_center Flutter 端按需同步原生状态的数据流:
topic_center 提供以下的 API,topic_center 遵循 Flutter 的多端一致性原则,咱们在三端提供了同样的 API,下图仅展现 dart 的 API 定义:
dart void putValue<T>(T value, String topic); Future<T> getValue<T>(String topic); Stream<T> getValueStream<T>(String topic); void putListValue<E>(E value, String topic); Future<List<E>> getListValue<E>(String topic); Stream<List<E>> getListValueStream<E>(String topic); void putMapValue<K, V>(Map<K, V> value, String topic); Future<Map<K, V>> getMapValue<K, V>(String topic); Stream<Map<K, V>> getMapValueStream<K, V>(String topic); void putTuple2Value<T0, T1>(Tuple2<T0, T1> value, String topic); Future<Tuple2<T0, T1>> getTuple2Value<T0, T1>(String topic); Stream<Tuple2<T0, T1>> getTuple2ValueStream<T0, T1>(String topic); void putTuple3Value<T0, T1, T2>(Tuple3<T0, T1, T2> value, String topic); Future<Tuple3<T0, T1, T2>> getTuple3Value<T0, T1, T2>(String topic); Stream<Tuple3<T0, T1, T2>> getTuple3ValueStream<T0, T1, T2>(String topic); void putTuple4Value<T0, T1, T2, T3>(Tuple4<T0, T1, T2, T3> value, String topic); Future<Tuple4<T0, T1, T2, T3>> getTuple4Value<T0, T1, T2, T3>(String topic); Stream<Tuple4<T0, T1, T2, T3>> getTuple4ValueStream<T0, T1, T2, T3>(String topic); void putTuple5Value<T0, T1, T2, T3, T4>(Tuple5<T0, T1, T2, T3, T4> value, String topic); Future<Tuple5<T0, T1, T2, T3, T4>> getTuple5Value<T0, T1, T2, T3, T4>(String topic); Stream<Tuple5<T0, T1, T2, T3, T4>> getTuple5ValueStream<T0, T1, T2, T3, T4>(String topic);
topic_center 是咱们在 架构 1.0 时提供的页面间通讯解决方案,后面会讲到咱们在进行架构升级以后提供的更轻量级的解决方案。
若是没有混合栈管理,咱们在原生应用上引入 Flutter 将是一个极为麻烦的事情,咱们可能为此维护比较混乱的 Channel 通讯层。
flutter_boost 是闲鱼开源的优秀的 Flutter 混合栈管理解决方案,也是当时社区惟一可选的解决方案。
flutter_boost 的优点:
若是不使用 flutter_boost,咱们的页面结构多是这样的:
使用了 flutter_boost 以后能够是这样的:
页面间通讯 1.0 的问题
topic_center 插件能解决页面间通讯的问题,但有一个不算问题的问题,对 topic 的管理成本太高。为了不全局 topic 重复的问题,每一个页面状态的同步都须要在 topic 上带上各类前缀,通常就是 模块、子模块、功能、页面做为前缀,而后这个 topic 最后长得跟页面的 url 极为类似。为了解决这个问题,须要想办法去掉这个 topic 的管理成本。
topic_center 这个库的投入产出比实在是不高,源码过于复杂 带来不仅是解决方案的复杂,也带来 维护成本推高 不少。
页面栈管理 1.0 的问题
好比,项目上须要实现关闭到某个页面的场景,或者删除当前页面之下的某个页面,咱们须要在 flutter_boost 上自行扩展,且难于维护,如何跟官方的 flutter_boost 保持代码同步是一个艰难的事情。
咱们在项目上大量使用页面回传参数的能力,可是该 API 在新版本上被移除了。
flutter_boost iOS 端的实现方案,在实际项目上使用时,咱们只能将每个 Flutter 页面都套在一个原生的 FlutterViewController 中 ,这直接致使每打开一个 Flutter 页面的内存占用高出 10M 左右。
为了解决这些问题,咱们开始了 架构 2.0 的建设。
架构 2.0 主要是解决 页面间通讯 1.0 和 页面栈管理 2.0 的解决方案存在的一些问题而演变出来的,同时对 页面模块化 作更细致的职能分解。
页面模块化 2.0
方案能够参考 ThrioModule,ThrioModule 的 API 也遵照多端一致性。
相比于 页面模块化 1.0,功能的变迁以下:
以上功能均提供三端一致的 API 2.0
页面栈路由 2.0
咱们开发了 thrio,主要是解决 页面间通讯 1.0 和 页面栈管理 1.0 中存在的问题。
thrio 的页面栈结构
thrio 的原理上改善点是除了复用 FlutterEngine,还复用了原生的页面容器,页面栈结构以下:
thrio 的路由
thrio 提供了三端一致的路由 API
页面的 push
dart ThrioNavigator.push(url: 'flutter1'); // 传入参数 ThrioNavigator.push(url: 'native1', params: { '1': {'2': '3'}}); // 是否动画,目前在内嵌的 dart 页面中动画没法取消,原生 iOS 页面有效果 ThrioNavigator.push(url: 'native1', animated:true); // 接收锁打开页面的关闭回调 ThrioNavigator.push( url: 'biz2/flutter2', params: {'1': {'2': '3'}}, poppedResult: (params) => verbose('biz2/flutter2 popped: $params'), );
objc [ThrioNavigator pushUrl:@"flutter1"]; // 接收所打开页面的关闭回调 [ThrioNavigator pushUrl:@"biz2/flutter2" poppedResult:^(id _Nonnull params) { ThrioLogV(@"biz2/flutter2 popped: %@", params); }];
kotlin ThrioNavigator.push(this, "biz1/flutter1", mapOf("k1" to 1), false, poppedResult = { Log.e("Thrio", "native1 popResult call params $it") } )
页面的 pop
dart ThrioNavigator.pop(params: 'popped flutter1'),
objc [ThrioNavigator popParams:@{@"k1": @3}];
kotlin ThrioNavigator.pop(this, params, animated)
页面的 popTo
dart ThrioNavigator.popTo(url: 'flutter1');
objc [ThrioNavigator popToUrl:@"flutter1" animated:NO];
kotlin ThrioNavigator.popTo(context, url, index)
页面的 remove
dart ThrioNavigator.remove(url: 'flutter1', animated: true);
objc [ThrioNavigator removeUrl:@"flutter1" animated:NO];
kotlin ThrioNavigator.remove(context, url, index)
thrio 的页面通知
页面通知做为解决页面间通讯的一个能力被引入 thrio,以一种很是轻量的方式解决了 topic_center 所要解决的问题,并且不须要管理 topic。
发送页面通知
dart ThrioNavigator.notify(url: 'flutter1', name: 'reload');
objc [ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];
kotlin ThrioNavigator.notify(url, index, params)
接收页面通知
使用 NavigatorPageNotify 这个 Widget 来实如今任何地方接收当前页面收到的通知。
dart NavigatorPageNotify( name: 'page1Notify', onPageNotify: (params) => verbose('flutter1 receive notify: $params'), child: Xxxx());
UIViewController实现协议NavigatorPageNotifyProtocol,经过 onNotify 来接收页面通知:
objc - (void)onNotify:(NSString *)name params:(NSDictionary *)params { ThrioLogV(@"native1 onNotify: %@, %@", name, params); }
Activity实现协议OnNotifyListener,经过 onNotify 来接收页面通知:
kotlin class Activity : AppCompatActivity(), OnNotifyListener { override fun onNotify(name: String, params: Any?) { } }
由于 Android activity 在后台可能会被销毁,因此页面通知实现了一个懒响应的行为,只有当页面呈现以后才会收到该通知,这也符合页面须要刷新的场景。
在咱们的业务上存在不少模块,进去以后是,首页 -> 列表页 -> 详情页 -> 处理页 -> 结果页,大体会是连续打开 5 个 Flutter 页面的场景。
这里会对 架构 1.0 和 架构 2.0 咱们所使用的解决方案作一些优劣对比,仅表示咱们业务场景下的结果,不同的场景不具有可参考性。
在此仅列出两个比较明显的改善措施,这些改善主要是原理层面的优点带来的,不表明 thrio 的实现比 flutter_boost 高明,另外数据仅供参考,只是为了说明原理带来的优点。
thrio 在 iOS 上的内存占用
一样连续打开 5 个页面的场景,boost 的方案会消耗 91.67M 内存,thrio 只消耗 42.76 内存,模拟器上跑出来的数据大体以下:
thrio 在 Android 上的页面打开速度
一样连续打开 5 个页面的场景,thrio 打开第一个页面跟 boost 耗时是同样的,由于都须要打开一个新的 Activity,以后 4 个页面 thrio 会直接打开 Flutter 页面,耗时会降下来,如下单位为 ms:
总的来讲,引入 Flutter 是一个很明智的选择,人效提高是很是明显的。若是你的 App 对包大小不敏感,那彻底能够尝试在项目中引入 Flutter。
固然过程当中也遇到了很是多的问题,但相对于人效提高来讲,解决这些问题的成本都是可接受的。
若是你想要无缝的将 Flutter 引入现有项目,thrio 可能会节省你不少精力。固然 thrio 是个很是年轻的库,相比于前辈 flutter_boost 还有很长的路要走,也欢迎有兴趣的同窗给 thrio 贡献代码。
做者:稻子,就任于哈啰出行。