看我用Android開發者聽得懂的語言解釋快應用頁面的生命周期和接口router-12.4
就像世界上***批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 }