iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 21
1
Software Development

Kotlin for Android系列 第 21

Day 21. Android Activity 生命週期 - 3/6

  今天一開始先來改良流程步驟追蹤的程式片段,因為使用很多重複的 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()

https://ithelp.ithome.com.tw/upload/images/20181104/20111944dOe2wK5DC4.png

  可以透過前方的秒數得知兩個觸發彼此的間隔時間。

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

https://ithelp.ithome.com.tw/upload/images/20181104/20111944GmABxZvfP2.png


  另外下方是啟動應用程式後,跳往第二頁面,再使用手機的返回鍵回到主頁面的流程觸發,可以發現程式設計時,在第二頁使用者做的任何編輯若沒有特別處理,當按下返回鍵就會被整個終結掉:

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 上方,主要只是用一個常數字串來儲存狀態的識別名稱。

https://ithelp.ithome.com.tw/upload/images/20181104/20111944nb2a8tqKmi.png

  接著實作 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()
   }
}

  執行模擬器看看是否成功吧!

https://ithelp.ithome.com.tw/upload/images/20181104/20111944QWB8BUldJB.png


  有關於生命週期的教學就到這邊,推薦各位可以到 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


上一篇
Day 20. Android Activity 生命週期 - 2/6
下一篇
Day 22. Android Activity Switch - 4/6
系列文
Kotlin for Android30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
SunAllen
iT邦研究生 1 級 ‧ 2018-11-04 16:48:29

背景圖超帥!/images/emoticon/emoticon12.gif

我要留言

立即登入留言