就像世界上***批Android工程师大多都是iOS工程师转行一样,世界上***批QuickApp工程师也大多都是Android工程师转行。将快应用知识与Android知识对比学习可以起到温故知新的效果。
查阅快应用官方文档可知快应用的“页面”和Android原生的Activity都是提供一个可以给用户来交互的屏幕,在底层也都是用Stack保存浏览记录。理解页面的生命周期就像理解Activity’的生命周期一样,有助于更好的组织页面的业务逻辑,方便页面之间的交互与资源释放等的处理。但为何“页面”仅有三种状态而Activity却有四种呢?又为何“页面”没有类似Activity的启动模式呢?本文将为你揭晓答案:
页面的生命周期和状态
众所众知Activity的生命周期由七个主要被动方法以及onBackPressed()、onNewIntent()、onActivityResult()、onSaveInstanceState()和onRestoreInstanceState()等其他被动方法组成,并且有、、和共四种状态;而查阅官方文档可知页面的被动方法仅有七个,而状态只有、和三种。我来给大家对比分析一下两组方法的对应关系。
onInit()和onReady()
根据快应用官方文档的说法:onInit()方法表示ViewModel的数据已经准备好,可以开始使用页面中的数据,能且仅能调用一次。onReady()方法表示ViewModel的模板已经编译完成,可以开始获取DOM节点,能且仅能调用一次。
每个Android开发者的类库里都有一个BaseActivity,这个BaseActivity里一般都有初始化配置、绑定View的同步方法onInitViews()和请求数据的异步方法onInitData();我们可以把onInit()理解为onInitData(),把onReady()理解为onInitViews()。
如果把眼光放远一点,拿页面与Fragment比较,onInit()更像Fragment的onCreate(),而onReady()更像onCreateView()。
onShow()和onHide()
每个快应用的App中可以同时运行多个页面,但是每次只能显示其中一个页面;这点不同于Android开发,可以同时显示多个Activity;也不同与纯前端开发,浏览器页面中每次只能有一个页面,当前页签打开另一个页面,上个页面就销毁了。
根据快应用官方文档的说法:页面被切换隐藏时调用onHide(),页面被切换重新显示时调用onShow()。很明显这与Activity有onStart()和onStop()、onResume()和onPause()两对方法不同,这是因为页面不像Activity有透明背景和Theme.Dialog主题,所以Activity的可见状态和前台状态在页面里仅对应显示状态。
onDestroy()
根据快应用官方文档的说法:onDestroy()方法在页面被销毁时调用,能且仅能调用一次。被销毁的可能原因有:用户从当前页面返回到上一页,或者用户打开了太多的页面,框架自动销毁掉部分页面,避免占用资源。而官方建议页面进入销毁状态时应该做一些释放资源的操作,这和Activity的onDestroy()方法的推荐使用方式不谋而合,所以页面的onDestroy()方法就是Activity的onDestroy()方法。
onBackPress()
根据快应用官方文档的说法:当用户点击实体BACK按键或左上角返回菜单时触发onBackPress()事件。我想没有人不会把页面的onBackPress()方法和Actvity的onBackPressed()方法联系到一起。
如果事件响应方法***返回true表示不返回,自己处理业务逻辑,完毕后开发者自行调用router.back()方法返回。代码如下:
onBackPress (params) { //做自己喜欢的事 return true }
对比一下Activity的onBackPressed()的override方式:
@Override public void onBackPressed() { // super.onBackPressed(); // 做自己喜欢的事 }
onMenuPress()
对比一下onBackPress()可知:当用户点击右上角菜单时触发onMenuPress()事件。如果我们有使用菜单的需求,可以通过manifest.json中的menu属性配置是否显示右上角的菜单。
所有支持快应用的国产Android设备的MENU键都用来清理内存,因此实体MENU键不会触发onMenuPress(),这点与onBackPress()有所区别。
页面路由接口router
根据快应用官方文档的说法:我们可以通过配置a组件的href属性跳转到应用内的页面,有点类似于Android开发中已不被推荐使用的的隐式Intent跳转Activity;此外我们也可以使用router接口,这就有点类似于Android开发的显式Intent组件或者ARouter框架。本文的一切页面跳转都使用router接口。
常见方法
接口router常见方法在官方文档里写得很清楚,我只讲几点注意事项:
(1)接口router的push()方法能跳转应用外的Activity包括电话、短信、邮件和其他快应用
(2)接口router的push()方法不能实现Android的Intent的“android.intent.category.HOME”标签的功能,也就是说,除非用户点HOME能回到桌面,否则开发者不能靠重写onBackPress()保留首页
(3)打开照相机、QQ聊天、微信分享、支付宝付款用的不是router
(4)back()方法的路径参数是path,并非push()和replace()的uri
回传参数的方式
传递参数的方式不在本文的讨论范围之内,但回传参数的方式却涉及生命周期,我们先看快应用回传参数的官方代码:
onHide () { // 页面被切换隐藏时,将要传递的数据对象写入全局变量 this.$app.$data.dataPageB = { gotoPage: 'pageA', params: { msg: this.msg } } },
对比一下Activity的setResult()方法的调用:
Intent intent = new Intent(); intent.putExtra("dataPageB",dataPageB); setResult(RESULT_OK,intent);
快应用接收回传参数的官方代码:
onShow () { // 页面被切换显示时,从数据中检查是否有页面B传递来的数据 if (this.$app.$data.dataPageB && this.$app.$data.dataPageB.gotoPage === 'pageA') { // 从数据中获取回传给本页面的数据 const data = this.$app.$data.dataPageB.params this.msg = data.msg } },
对比一下Activity的onActivityResult()方法:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == pageA && resultCode == RESULT_OK){ this.msg = ((BaseBean)data.getSerializableExtra("dataPageB")).getMessage(); } }
由此可见,在onRscume()方法里检验全局变量的变化这一行为,作为Android原生开发中饱受诟病的新手行为,在快应用开发中是官方推荐的,所以快应用不需要类似onActivityResult()方法的方法。
研究接口router和页面生命周期关系的实践
“纸上得来终觉浅”,我们写一个LifecycleDemo来研究接口router和页面生命周期关系:
首先打开这个LifecycleDemo,我们可以看到logcat打印出如下信息:
### 页面A onInit ### ### 页面A onReady ### ### 页面A onShow ### 当前页面在页面栈中的位置 : 1/1
点击BACK键,返回桌面,logcat打印出如下信息:
### 页面A onBackPress ### ### 页面A onHide ### ### 页面A onDestroy ###
与官方文档描述相同,符合预期
打开其他Activity
接下来我们打开其他Activity,包括系统桌面、打电话界面和其他应用
点击HOME键,然后热启动LifecycleDemo,Logcat打印如下:
### 页面A onHide ### ### 页面A onShow ### 当前页面在页面栈中的位置 : 1/1
应用内打开其他系统Activity,然后热启动LifecycleDemo,Logcat打印如下:
### 页面A onHide ### ### 页面A onShow ### 当前页面在页面栈中的位置 : 1/1
应用内打开别的快应用,然后热启动LifecycleDemo,Logcat打印如下:
### 页面A onHide ### ### 页面A onShow ### 当前页面在页面栈中的位置 : 1/1
结论:符合预期,支持上文onShow()相当于onStart()和onResume(),onHide()相当于onPause()和onStop()的猜想。
用push()方法进行应用内页面跳转
用push()方法跳转到页面A,logcat打印如下:
### 页面A onHide ### ### 页面A onInit ### ### 页面A onReady ### ### 页面A onShow ### 当前页面在页面栈中的位置 : 2/2
显然页面栈里的顺序为AA,支持上文页面的启动模式相当于Activity的Standard模式的猜想。现在猜想***个A是前面的,第2、3、4个A是后面的。我们接着用push()方法跳转到页面B,logcat打印如下:
### 页面A onHide ### ### 页面B onInit ### ### 页面B onReady ### ### 页面B onShow ### 当前页面在页面栈中的位置 : 3/3
显然页面栈里的顺序为AAB,也符合预期,支持上文猜想。
我们发现快应用官方文档存在歧义,就是首页究竟是指稳定运行时页面栈底的页面(类似Android原生开发的MainActivity),还是指manifest.json文件中“router.entry”对应的页面(类似AndroidManifest.xml文件中带“android.intent.action.MAIN"标签的Activity,通常被命名为SplashActivity),我们验证一下:
当“router.entry”对应页面A,而页面栈里页面顺序为BBAACC的时候,我们用push()方法跳转到首页。首页是这样的:
而logcat打印如下:
### 页面C onHide ### ### 页面A onInit ### ### 页面A onReady ### ### 页面A onShow ### 当前页面在页面栈中的位置 : 7/7
原来接口router可以跳转的首页指的是“router.entry”对应的页面。
用replace()方法进行应用内页面跳转
当页面栈里仅有A的情况下,用replace()方法跳转到页面A,logcat打印如下:
### 页面A onHide ### ### 页面A onDestroy ### ### 页面A onInit ### ### 页面A onReady ### ### 页面A onShow ### 当前页面在页面栈中的位置 : 1/1
显然页面栈里仅有一个A,猜想replace()方法类似Activity里的这段代码:
startActivity(intent); finish();
又猜想第1、2个A是前面的,第3、4、5个A是后面的。我们接着用replace()方法跳转到页面B,logcat打印如下:
### 页面A onHide ### ### 页面A onDestroy ### ### 页面B onInit ### ### 页面B onReady ### ### 页面B onShow ### 当前页面在页面栈中的位置 : 1/1
符合预期,支持上文猜想。
用back()方法进行应用内页面跳转
在页面栈里的顺序为AABBCC的情况下,根据文档仅能得出用back()方法返回上一页后页面栈里的顺序为AABBC,返回页面B后页面栈里的顺序为AABB,返回页面A或首页后页面栈里的顺序为AA,有点类似Intent的FLAG_ACTIVITY_CLEAR_TASK标签。我们只讨论官方文档忽略的内容:
我们用back()方法跳转到页面C,页面无变化;在页面栈里的顺序为ABCABC的情况下,我们用back()方法跳转到页面C,页面也无变化。得出back()方法不能用来跳转到栈顶页面的结论。
总结
本文中获得的有关快应用页面生命周期的知识和经验的总结如下:
(1)页面可以理解为Activity,并且启动模式能且仅能为standard
(2)页面的onInit()和onReady()可以分别理解为你的BaseActivity的onInitData()和 onInitViews()。
(3)Activity的可见状态和前台状态在页面里都是显示状态,所以onShow()可以理解为onStart()和onResume(),同理onHide()可以理解为onPause()和onStop()
(4)页面的onDestroy()里可以理解为Activity的onDestroy()
(5)快应用没有singleTop这种启动模式,自然没有onNewIntent()方法,但用replace()方法启动栈顶页面可以起到同样效果。
(6)onActivityResult()、onSaveInstanceState()和onRestoreInstanceState()和也都没有对应方法
(7)onBackPress()是BACK键触发的方法,可以被拦截,但无法改成HOME键的效果
(8)onMenuPress()方法不是MENU键触发的方法
(9)快应用没有singleTask这种启动模式,但back()方法起到类似Intent的FLAG_ACTIVITY_CLEAR_TASK的作用。
(10)back()方法不能用来跳转到栈顶页面。
(11)官方文档中所有的“首页”都指manifest.json文件中“router.entry”对应的页面(类似AndroidManifest.xml文件中带“android.intent.action.MAIN"标签的Activity,通常被命名为SplashActivity),而不是指指稳定运行时页面栈底的页面(类似Android原生开发的MainActivity)
附录:本文完整代码
页面A(文件路径:…/src/PageA/index.ux)的完整代码(B、C的代码仅title不同):
<template>
<div class="doc-page">
<text class="title">欢迎打开{{title}}</text>
<text class='text' if="{msg}">{{msg}}</text>
<input type="button" class="btn" onclick="this.$app.$def.routePush('/PageA')" value="用push()方法跳转到页面A" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('/PageB')" value="用push()方法跳转到页面B" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('/PageC')" value="用push()方法跳转到页面C" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('/')" value="用push()方法跳转到首页" />
<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/PageA')" value="用replace()方法跳转到页面A" />
<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/PageB')" value="用replace()方法跳转到页面B" />
<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/PageC')" value="用replace()方法跳转到页面C" />
<input type="button" class="btn" onclick="this.$app.$def.routeReplace('/')" value="用replace()方法跳转到首页" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack('/PageA')" value="用back()方法跳转到页面A" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack('/PageB')" value="用back()方法跳转到页面B" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack('/PageC')" value="用back()方法跳转到页面C" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack('/')" value="用back()方法跳转到首页" />
<input type="button" class="btn" onclick="this.$app.$def.routeBack()" value="用back()方法返回上一页" />
<input type="button" class="btn" onclick="routeClear()" value="只保留当前页面" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('tel:10086')" value="跳转到打电话页面" />
<input type="button" class="btn" onclick="this.$app.$def.routePush('hap://app/me.ele.xyy/')" value="跳转到指定快应用(饿了么)" />
</div>
</template>
<style>
@import '../Common/css/common.css';
.title {
font-size: 40px;
text-align: center;
}
.text {
font-size: 30px;
text-align: center;
}
</style>
<script>
import router from '@system.router'
export default {
private: {
msg:'',
title: '页面A',
},onInit () {
this.$page.setTitleBar({text: this.title})
console.error(`### `+this.title+` onInit ###`)
this.msg = ""
},
onReady () {
console.error(`### `+this.title+` onReady ###`)
},
onShow () {
console.error(`### `+this.title+` onShow ###`)
this.msg = this.$app.$def.routeInfo()
console.error(`${this.msg}`)
},
onHide () {
console.error(`### `+this.title+` onHide ###`)
},
onDestroy () {
console.error(`### `+this.title+` onDestroy ###`)
},
onBackPress (params) {
console.error(`### `+this.title+` onBackPress ###`)
},
onMenuPress () {
console.error(`### `+this.title+` onMenuPress ###`)
},
routeClear() {
this.$app.$def.routeClear()
this.msg = this.$app.$def.routeInfo()
console.error(`${this.msg}`)
}
}
</script>
工具类util.js的完整代码:
import router from '@system.router' function routePush(uri,params) { // 跳转到应用内的某个页面,或其他Activity // 匹配到与路径与uri相同的页面,则跳转到该页面,否则跳转到首页 // 参数为"/",跳转到首页 // uri若为包含schema的完整uri,则跳转到应用外的Activity(目前仅支持电话、短信、邮件和其他快应用) // params为传递的参数,不在本文讨论范围内 router.push ({ uri: uri, params: params }) } function routeReplace(uri,params) { // 跳转到应用内的某个页面,同时关闭当前页面 // 除了不能跳转到应用外的页面,一切同push()方法 router.replace ({ uri: uri, params: params }) } function routeBack(path) { // 跳转到应用内的某个已经打开过的页面,同时关闭当前页面 // 不传参数,或没有匹配到对应页面,则返回上一个页面 // 参数为"/",返回首页 // 若匹配到多个页面,返回至***打开的页面 // 注意back()方法的参数是path而不是uri router.back ({ path: path }) } function routeInfo (){ // 用getState()方法获取当前页面状态,index表示当前页面在页面栈中的位置(计数从0开始) // 用getLength()方法获取当前页面栈的页面数量 return `当前页面在页面栈中的位置 : `+ (router.getState().index + 1) + `/` + router.getLength() } function routeClear (){ // 清空所有历史页面记录,仅保留当前页面 router.clear() } export default { routeReplace, routePush, routeBack, routeInfo, routeClear }