作者:闲鱼技术——皓黯
在经历了近两个月的开发以及内部测试与线上灰度,FlutterBoost3.0的preview版本终于与大家见面了,与beta版本相比,这个版本在不大动主体结构的基础上,增加了以下能力:
- 重构生命周期,确保生命周期语义准确
- 双端一致性近一步对齐
- 增加自定义的启动参数
- 实现页面返回传参方案
- 支持页面透明能力
- 增加自定义事件发送机制
- 增加前置拦截器
- 提供更完善的文档与例子
由于篇幅有限,就不对以上能力一一展开了。如果对具体实现感兴趣的话,可以通过源码和文档来了解相应的功能,我们今天先来聊一聊生命周期部分的设计与实现、文档与用例和社区建设。
1. 页面生命周期设计
FlutterBoost3.0有两个和页面相关的概念,一个是BoostContainer,另一个是BoostPage。每个BoostContainer在Native层都会有一个与之对应的容器,比如在Android端,这个容器可能是FlutterBoostActivity或者FlutterBoostFragment,而在iOS端,这个容器则指的是FBFlutterViewContainer。每个BoostContainer都会有一个与之对应的Navigator,因而一个BoostContainer可以包含多个BoostPage,这个设计也被我们称之为双重栈。细心的同学可能已经发现了,当我们需要打开一个Flutter页面时,有一个额外的参数withContainer
,这个参数设置为false时,表示打开页面时不需要打开一个新容器,而这个参数如果被设置为true,则会在打开页面的同时,去打开一个新容器。
在开发业务的过程中,我们常常需要知道一个Flutter页面的可见性,比如我们写了一个Flutter页面,里面有一个VideoWidget,我们希望VideoWidget在页面可见的时候播放,在页面不可见的时候暂停。对页面可见性变化的监听是FlutterBoost需要提供的核心能力之一,因此我们提供了一个名为PageVisibilityObserver的API,用于专门监听页面可见性变化。
在一开始的设计当中,我们是通过上层Dart代码来掌控页面生命周期的。我们在FlutterBoostAppState里,维护了一个List\<BoostContaner\>,而在每个BoostContainer中,则维护了一个List\<BoostPage\>,所以显示的页面实际上就是顶部BoostContainer中的顶部BoostPage,所以每次顶部的BoostContainer发生变化,或者顶部BoostContainer中的顶部BoostPage发生变化,我们就认为发生了页面可见性变化,并将相应事件分发下去。
然而这个方案在实践的过程中,发现了一些问题,由于Dart侧只对Flutter的双层栈有感知,而对Native侧的页面栈则是毫无感知的,所以哪怕一个页面是顶部BoostContainer中的顶部BoostPage,也不见得这个页面就处于显示状态。举个例子,比如Flutter页面A去打开一个Native页面B,那么在打开Native页面B之后,Flutter页面A还是在双重栈顶部,因此如果不做任何处理,那么FlutterBoost还是认为Flutter页面A是可见的。我们也尝试了一些手段去解觉这些Bad Case,比如在Native层也维护一个Flutter页面栈用来感知页面显示隐藏等。但是在后续的验证过程中,我们发现这个方案可维护性不高,最终放弃了。
之后我们决定还是采用更稳妥的方案,BoostContainer的显示和隐藏,让Native容器自己来控制,当Native容器展示的时候发出onContainerShow事件通知对应的BoostContainer展示,Native容器隐藏时再发出onContainerHide事件通知对应的的BoostContainer隐藏,这样生命周期难以维护的问题也就解决了,这个方案的可维护性相比上一个方案要稳定,但是这个方案也有一个不得不解决的问题,第一次onContainerShow可能会收不到。我们以Android为例,FlutterBoostActivity创建后的第一次onResume发出了onContainerShow事件,然而该事件并不一定会被收到,因为此时可能页面对应的Widget还没有完成创建。稳定的生命周期事件是一个强诉求,所以这个问题我们需要在FlutterBoost3.0彻底解决。那么这个问题该如何解决呢?为了解决这个问题,我们将第一次收到onContainerShow事件单独处理,具体逻辑如下
void containerDidShow(BoostContainer container) {
final id = container.pageInfo.uniqueId;
assert(id != null);
if (!hasShownPageIds.contains(id)) {
hasShownPageIds.add(id);
// This case indicates it is the first time that this container show
// So we should dispatch event using
// PageVisibilityBinding.dispatchPageShowEventOnPageShowFirstTime
// to ensure the page will receive callback
PageVisibilityBinding.instance
.dispatchPageShowEventOnPageShowFirstTime(container.topPage.route);
} else {
PageVisibilityBinding.instance
.dispatchPageShowEvent(container.topPage.route);
}
}
而在PageVisibilityBinding.dispatchPageShowEventOnPageShowFirstTime中,我们将事件分发延迟到这一帧渲染结束
///When page show first time,we should dispatch event in [FrameCallback]
///to avoid the page can't receive the show event
void dispatchPageShowEventOnPageShowFirstTime(Route<dynamic> route) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
dispatchPageShowEvent(route);
});
}
这样我们就能确保第一次onContainerShow事件能够被接收到。从最后的代码来看,这是一个微不足道的改动,但是从设计上看,这个改动明确定义了页面生命周期事件,确保了生命周期事件一定被调用到。
2. Flutter应用生命周期设计
对于纯Flutter应用来说,一个Flutter应用实际上是跑在一个Activity或者ViewController上的,所以Flutter应用的生命周期实际上也和Activity或ViewController的生命周期绑定了,这也是Flutter目前的实现。但是对于单引擎多页面的混合栈场景来说,这无疑是不适用的。如果我们不对Flutter应用生命周期进行任何处理,那么就有可能出现Flutter应用生命周期混乱,最终导致界面无法刷帧。之前我们解决这个问题的思路,都是从Native层出发,通过控制生命周期事件的发送,来校正Flutter应用的生命周期。比如Android端,就是在每次Flutter应用被pause的时候,补发一个resume事件,而iOS端则是绕过FlutterViewController生命周期方法来自行管理生命周期事件。这样做虽然校正了Flutter应用的生命周期,但最终也导致了上层应用的生命周期非常混乱,可能会出现pause和resume连续多次被调用的情况。
为了上层使用者能有一个稳定的Flutter应用生命周期,我们决定将这部分能力接管。我们提供了一个BoostFlutterBinding,用于接管生命周期,相关代码如下
mixin BoostFlutterBinding on WidgetsFlutterBinding {
bool _appLifecycleStateLocked = true;
@override
void initInstances() {
super.initInstances();
_instance = this;
changeAppLifecycleState(AppLifecycleState.resumed);
}
static BoostFlutterBinding get instance => _instance;
static BoostFlutterBinding _instance;
@override
void handleAppLifecycleStateChanged(AppLifecycleState state) {
if (_appLifecycleStateLocked) {
return;
}
Logger.log('boost_flutter_binding: handleAppLifecycleStateChanged ${state.toString()}');
super.handleAppLifecycleStateChanged(state);
}
void changeAppLifecycleState(AppLifecycleState state) {
if (SchedulerBinding.instance.lifecycleState == state) {
return;
}
_appLifecycleStateLocked = false;
handleAppLifecycleStateChanged(state);
_appLifecycleStateLocked = true;
}
}
代码非常简单,就是将handleAppLifecycleStateChanged方法给hook掉,这样无论Native层如何调用Flutter设定的生命周期事件,都不会影响到上层Flutter应用的生命周期。另外我们提供了一个changeAppLifecycleState方法,这个方法可以真正来改变上层Flutter应用的生命周期,目前这个方法的调用时机与Flutter容器个数相关,当容器大于等于1,Flutter应用的生命周期状态为resumed,而容器个数为0时,Flutter应用的生命周期状态则为paused。
3. 文档与用例完善
FlutterBoost在之前只有一份接入文档,这对使用者来说并不算友好。因此我们决定提供更完善的文档。我们梳理了大家最关心的几个主题:
- 集成详细步骤
- 基本的路由API
- 页面生命周期监测相关API
- 自定义发送跨端事件API
对于这些主题,我们这次都提供了相对应的文档,让使用者接入FlutterBoost可以更轻松,在遇到问题的时候也能更容易排查。另外我们也正在编写全新的example3.0,配合文档使用,在这个新的example3.0中,我们会把所有文档中提到的部分都以用例的方式呈现出来,希望这能帮助到大家。
4. 社区建设
之后Boost所有的开发计划工作,都会在project看板上体现出来,对应的地址为( https://github.com/alibaba/flutter_boost/projects)。对我们工作感兴趣的同学,可以在看板上看到我们的工作规划,欢迎一起交流探讨。另外如果有哪些想要我们支持的feature,也可以提issue给我们,如果我们评估合理,也会加入到看板上。
于此同时,我们增加了一个AUTHORS文件(https://github.com/alibaba/flutter_boost/blob/master/AUTHORS),记录所有FlutterBoost的贡献者(Contributors)。之后给我们提PR的同学,在提供PR的同时,也可以在这个文件上,加上你的名字,感谢大家为Boost做出的贡献。
5. 未来展望
FlutterBoost3.0作为AliFlutter的核心基础设施,目前主要由闲鱼团队和UC Hummer团队进行开发维护,主要开发者包括noborder、0xZOne、christyuj、ColdPaleLight、luckysmg,另外也要感谢seedotlee、CheungSKei、bktoky、jk等同学为FlutterBoost3.0做出的贡献,目前集团内已有多个App接入了FlutterBoost3.0,包括夸克、淘宝联盟、吃货笔记等。
之后FlutterBoost3.0的preview版本原则上不会再做Breaking Change,目前已经使用了beta版的同学,也可以陆续切到preview版本上了,下个阶段我们会将工作重心放在issue收敛与文档用例补齐上。
FlutterBoost作为一个开源项目,还有很长的路要走,感谢大家一路支持和包容,我们也希望有更多同学能参与到这个项目中来。