今天一開始先來改良流程步驟追蹤的程式片段,因為使用很多重複的 Log.d
很雜亂,應該要將它們整合一下,在 FlowActivity 加入一個函式,使用變數 o
接收 () -> Unit
的型態值,這裡有稍微用到 lambda,並將原本使用於取得流程的 {}.javaClass.enclosingMethod
修改一下:
fun showStep(o: () -> Unit) {
Log.d(TAG, "頁面:${javaClass.simpleName},流程:${o.javaClass.enclosingMethod!!.name}")
}
接著就可將昨天程式碼有用到舊方式的部分,都調整呼叫新的函式:showStep {}
,請注意因為是使用到 lambda 方式,所以要採用 {}
括號。
open class FlowActivity : AppCompatActivity() {
val TAG = "Flow"
override fun onCreate(savedInstanceState: Bundle?) {
showStep { }
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_flow)
}
override fun onStart() {
showStep { }
super.onStart()
}
override fun onResume() {
showStep { }
super.onResume()
}
override fun onRestart() {
showStep { }
super.onRestart()
}
override fun onPause() {
showStep { }
super.onPause()
}
override fun onStop() {
showStep { }
super.onStop()
}
override fun onDestroy() {
showStep { }
super.onDestroy()
}
fun showStep(o: () -> Unit) {
Log.d(TAG, "頁面:${javaClass.simpleName},流程:${o.javaClass.enclosingMethod!!.name}")
}
}
繼續往下說明展示更多生命週期的循環,可以透過模擬器上的 More 選項,模擬一下來電,當有來電通知時,並未觸發 onPause()
,要等到按下接聽時,就會觸發暫停,等到畫面完全轉移到通話畫面,就會觸發 onStop()
。
可以透過前方的秒數得知兩個觸發彼此的間隔時間。
01.821s D/Flow: 頁面:MainActivity,流程:onPause
04.286s D/Flow: 頁面:MainActivity,流程:onStop
通話完成回到頁面時,就會重新開始生命週期。
D/Flow: 頁面:MainActivity,流程:onRestart
D/Flow: 頁面:MainActivity,流程:onStart
D/Flow: 頁面:MainActivity,流程:onResume
這邊須注意的點是,在不同的 Android 版本上,流程的行為未必會相同,在更早之前的版本接通電話時,只會進入 onPause 不會到 onStop。
接著再按一下手機上的總覽按鈕 (最右邊),此時會觸發 onPause > onStop,再把應用程式向上移除,最終就會出現 onDestroy:
D/Flow: 頁面:MainActivity,流程:onPause
D/Flow: 頁面:MainActivity,流程:onStop
D/Flow: 頁面:MainActivity,流程:onDestroy
另外下方是啟動應用程式後,跳往第二頁面,再使用手機的返回鍵回到主頁面的流程觸發,可以發現程式設計時,在第二頁使用者做的任何編輯若沒有特別處理,當按下返回鍵就會被整個終結掉:
D/Flow: 頁面:MainActivity,流程:onCreate
D/Flow: 頁面:MainActivity,流程:onStart
D/Flow: 頁面:MainActivity,流程:onResume
// 點擊 Go 按鈕,前往第二頁
D/Flow: 頁面:MainActivity,流程:onPause
D/Flow: 頁面:SecondActivity,流程:onCreate
D/Flow: 頁面:SecondActivity,流程:onStart
D/Flow: 頁面:SecondActivity,流程:onResume
D/Flow: 頁面:MainActivity,流程:onStop
// 點擊返回按鈕
D/Flow: 頁面:SecondActivity,流程:onPause
D/Flow: 頁面:MainActivity,流程:onRestart
D/Flow: 頁面:MainActivity,流程:onStart
D/Flow: 頁面:MainActivity,流程:onResume
D/Flow: 頁面:SecondActivity,流程:onStop
D/Flow: 頁面:SecondActivity,流程:onDestroy
這部分可以進階閱讀:暫停並繼續應用行為顯示。
- 停止動畫或會耗用 CPU 資源的其他進行中行為。
- 認可未儲存的變更,但是只有在使用者希望離開時永久儲存此類變更 (例如電子郵件草稿) 的情況下才執行此操作。
- 釋放系統資源,例如廣播接收器、感應器 (如 GPS) 的控點,或釋放在您的應用行為顯示暫停時可能影響電池使用壽命 (且使用者不再需要) 的資源。
另外也可以觀察一下,打開應用程式,前往第二頁面後透過總覽將程式終止,會發現 Destory 是來自主頁面發出的,對於日後設計程式碼時,能夠了解要實作在哪個目標上才會被觸發。
D/Flow: 頁面:MainActivity,流程:onCreate
D/Flow: 頁面:MainActivity,流程:onStart
D/Flow: 頁面:MainActivity,流程:onResume
// 前往第二頁
D/Flow: 頁面:MainActivity,流程:onPause
D/Flow: 頁面:SecondActivity,流程:onCreate
D/Flow: 頁面:SecondActivity,流程:onStart
D/Flow: 頁面:SecondActivity,流程:onResume
D/Flow: 頁面:MainActivity,流程:onStop
// 自第二頁使用總覽終止程式
D/Flow: 頁面:SecondActivity,流程:onPause
D/Flow: 頁面:SecondActivity,流程:onStop
D/Flow: 頁面:MainActivity,流程:onDestroy
接著要介紹的是 InstanceState,各位讀者可以試試看在模擬機上,或按下翻轉螢幕,在 Logcat 上會發現,翻轉後 Activity 會被終止再重新啟動。
D/Flow: 頁面:MainActivity,流程:onCreate
D/Flow: 頁面:MainActivity,流程:onStart
D/Flow: 頁面:MainActivity,流程:onResume
// 螢幕翻轉
D/Flow: 頁面:MainActivity,流程:onPause
D/Flow: 頁面:MainActivity,流程:onStop
D/Flow: 頁面:MainActivity,流程:onDestroy
D/Flow: 頁面:MainActivity,流程:onCreate
D/Flow: 頁面:MainActivity,流程:onStart
D/Flow: 頁面:MainActivity,流程:onResume
這樣的機制會導致什麼結果呢?我們可以使用一個全域變數計數來觀察:
(這裡暫時先借用 goBtn 來展示,在之前章節設計的程式碼可以先註解起來,後面還會用到)
class MainActivity : FlowActivity() {
var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
goBtn.setOnClickListener {
Log.d(TAG, "count:${++count}")
}
}
}
D/Flow: 頁面:MainActivity,流程:onCreate
D/Flow: 頁面:MainActivity,流程:onStart
D/Flow: 頁面:MainActivity,流程:onResume
D/Flow: count:1
D/Flow: count:2
D/Flow: count:3
// 旋轉
D/Flow: 頁面:MainActivity,流程:onPause
D/Flow: 頁面:MainActivity,流程:onStop
D/Flow: 頁面:MainActivity,流程:onDestroy
D/Flow: 頁面:MainActivity,流程:onCreate
D/Flow: 頁面:MainActivity,流程:onStart
D/Flow: 頁面:MainActivity,流程:onResume
D/Flow: count:1
可以發現,一旦旋轉螢幕就會讓這個值重新初始化,這對程式設計會造成不便,要解決這個問題可以實作 onSaveInstanceState()
:
override fun onSaveInstanceState(savedInstanceState: Bundle) {
// 儲存狀態,STATE_COUNT 為狀態名稱常數,可參考下圖
savedInstanceState.putInt(STATE_COUNT, count)
// 請始終呼叫超級類別實作,以便預設實作可儲存檢視階層的狀態
super.onSaveInstanceState(savedInstanceState)
}
其中 STATE_COUNT
可以宣告於 class
上方,主要只是用一個常數字串來儲存狀態的識別名稱。
接著實作 onRestoreInstanceState()
,將儲存的狀態取回:
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
// 請始終呼叫超級類別實作,以便預設實作可還原檢視階層的狀態。
super.onRestoreInstanceState(savedInstanceState)
// 將儲存的狀態取回
count = savedInstanceState.getInt(STATE_COUNT)
}
另外,我們也可以在這兩個函式的第一行插入 showStep {}
,以便觀察步驟流程,現在可以再次執行程式,操作看看結果會有什麼不同:
D/Flow: count:1
D/Flow: count:2
D/Flow: count:3
// 旋轉
D/Flow: 頁面:MainActivity,流程:onPause
D/Flow: 頁面:MainActivity,流程:onStop
D/Flow: 頁面:MainActivity,流程:onSaveInstanceState
D/Flow: 頁面:MainActivity,流程:onDestroy
D/Flow: 頁面:MainActivity,流程:onCreate
D/Flow: 頁面:MainActivity,流程:onStart
D/Flow: 頁面:MainActivity,流程:onRestoreInstanceState
D/Flow: 頁面:MainActivity,流程:onResume
D/Flow: count:4
本章節最後要以實作 finish()
為結尾,當按下手機上的返回鍵時,會觸發 finish
方法,許多應用程式上常見二次確認是否離開,設計方式是採用第一次按下返回鍵時,記錄系統當下的時間,等到使用者第二次觸發時,若在指定的時間差內就視為確定關閉,否則當成使用者誤觸,設計邏輯如下:
var lastTime: Long = 0
override fun finish() {
// 記錄每次觸發的時間
val currentTime = System.currentTimeMillis()
// 計算時間差
if(currentTime - lastTime > 3 * 1000) {
// 儲存這一次的時間
lastTime = currentTime
Toast.makeText(this, "再按一下離開,我們明天見!", Toast.LENGTH_SHORT).show()
} else {
// 離開
super.finish()
}
}
執行模擬器看看是否成功吧!
有關於生命週期的教學就到這邊,推薦各位可以到 Android 開發者官方網站-管理應用行為顯示生命週期,閱讀更多的相關知識,該網頁下方提供4個課程,對 Activity 生命周期與相對應程式處理,能有更詳細的進階說明,明天將往第二個 Activity 頁面邁進,讓我們明天見!
資料參考
Understand the Activity Lifecycle-Android Developers
https://developer.android.com/guide/components/activities/activity-lifecycle管理應用行為顯示生命週期-Android Developers (中文)
https://developer.android.com/training/basics/activity-lifecycle重新建立應用行為顯示
https://developer.android.com/training/basics/activity-lifecycle/recreating?hl=zh-tw