在上一篇文章中,我們從常見的 StatefulWidget 的“萬金油”誤區出發,一起回顧了 Widget 的 UI 更新機制。
通過父 Widget 初始化時傳入的靜態配置,StatelessWidget 就能完全控制其靜態展示。而 StatefulWidget,還需要借助于 State 對象,在特定的階段來處理用戶的交互或其內部數據的變化,并體現在 UI 上。這些特定的階段,就涵蓋了一個組件從加載到卸載的全過程,即生命周期。與 iOS 的 ViewController、Android 的 Activity 一樣,Flutter 中的 Widget 也存在生命周期,并且通過 State 來體現。
而 App 則是一個特殊的 Widget。除了需要處理視圖顯示的各個階段(即視圖的生命周期)之外,還需要應對應用從啟動到退出所經歷的各個狀態(App 的生命周期)。
對于開發者來說,無論是普通 Widget(的 State)還是 App,框架都給我們提供了生命周期的回調,可以讓我們選擇恰當的時機,做正確的事兒。所以,在對生命周期有了深入理解之后,我們可以寫出更加連貫流暢、體驗優良的程序。
那么,今天我就分別從 Widget(的 State)和 App 這兩個維度,與你介紹它們的生命周期。
## State 生命周期
State 的生命周期,指的是在用戶參與的情況下,其關聯的 Widget 所經歷的,從創建到顯示再到更新最后到停止,直至銷毀等各個過程階段。
這些不同的階段涉及到特定的任務處理,因此為了寫出一個體驗和性能良好的控件,正確理解 State 的生命周期至關重要。
State 的生命周期流程,如圖 1 所示:
:-: 
圖 1 State 生命周期圖
可以看到,State 的生命周期可以分為 3 個階段:創建(插入視圖樹)、更新(在視圖樹中存在)、銷毀(從視圖樹中移除)。接下來,我們一起看看每一個階段的具體流程。
### 創建
State 初始化時會依次執行 :構造方法 -> initState -> didChangeDependencies -> build,隨后完成頁面渲染。
我們來看一下初始化過程中每個方法的意義。
* 構造方法是 State 生命周期的起點,Flutter 會通過調用 StatefulWidget.createState() 來創建一個 State。我們可以通過構造方法,來接收父 Widget 傳遞的初始化 UI 配置數據。這些配置數據,決定了 Widget 最初的呈現效果。
* initState,會在 State 對象被插入視圖樹的時候調用。這個函數在 State 的生命周期中只會被調用一次,所以我們可以在這里做一些初始化工作,比如為狀態變量設定默認值。
* didChangeDependencies 則用來專門處理 State 對象依賴關系變化,會在 initState() 調用結束后,被 Flutter 調用。
* build,作用是構建視圖。經過以上步驟,Framework 認為 State 已經準備好了,于是調用 build。我們需要在這個函數中,根據父 Widget 傳遞過來的初始化配置數據,以及 State 的當前狀態,創建一個 Widget 然后返回。
### 更新
Widget 的狀態更新,主要由 3 個方法觸發:setState、didchangeDependencies 與 didUpdateWidget。
接下來,我和你分析下這三個方法分別會在什么場景下調用。
* setState:我們最熟悉的方法之一。當狀態數據發生變化時,我們總是通過調用這個方法告訴 Flutter:“我這兒的數據變啦,請使用更新后的數據重建 UI!”
* didChangeDependencies:State 對象的依賴關系發生變化后,Flutter 會回調這個方法,隨后觸發組件構建。哪些情況下 State 對象的依賴關系會發生變化呢?典型的場景是,系統語言 Locale 或應用主題改變時,系統會通知 State 執行 didChangeDependencies 回調方法。
* didUpdateWidget:當 Widget 的配置發生變化時,比如,父 Widget 觸發重建(即父 Widget 的狀態發生變化時),熱重載時,系統會調用這個函數。
一旦這三個方法被調用,Flutter 隨后就會銷毀老 Widget,并調用 build 方法重建 Widget。
### 銷毀
組件銷毀相對比較簡單。比如組件被移除,或是頁面銷毀的時候,系統會調用 deactivate 和 dispose 這兩個方法,來移除或銷毀組件。
接下來,我們一起看一下它們的具體調用機制:
* 當組件的可見狀態發生變化時,deactivate 函數會被調用,這時 State 會被暫時從視圖樹中移除。值得注意的是,頁面切換時,由于 State 對象在視圖樹中的位置發生了變化,需要先暫時移除后再重新添加,重新觸發組件構建,因此這個函數也會被調用。
* 當 State 被永久地從視圖樹中移除時,Flutter 會調用 dispose 函數。而一旦到這個階段,組件就要被銷毀了,所以我們可以在這里進行最終的資源釋放、移除監聽、清理環境,等等。
如圖 2 所示,左邊部分展示了當父 Widget 狀態發生變化時,父子雙方共同的生命周期;而中間和右邊部分則描述了頁面切換時,兩個關聯的 Widget 的生命周期函數是如何響應的。
:-: 
圖 2 幾種常見場景下 State 生命周期圖
我準備了一張表格,從功能,調用時機和調用次數的維度總結了這些方法,幫助你去理解、記憶。
:-: 
圖 3 State 生命周期中的方法調用對比
另外,我強烈建議你打開自己的 IDE,在應用模板中增加以上回調函數并添加打印代碼,多運行幾次看看各個函數的執行順序,從而加深對 State 生命周期的印象。畢竟,實踐出真知。
## App 生命周期
視圖的生命周期,定義了視圖的加載到構建的全過程,其回調機制能夠確保我們可以根據視圖的狀態選擇合適的時機做恰當的事情。而 App 的生命周期,則定義了 App 從啟動到退出的全過程。
在原生 Android、iOS 開發中,有時我們需要在對應的 App 生命周期事件中做相應處理,比如 App 從后臺進入前臺、從前臺退到后臺,或是在 UI 繪制完成后做一些處理。
這樣的需求,在原生開發中,我們可以通過重寫 Activity、ViewController 生命周期回調方法,或是注冊應用程序的相關通知,來監聽 App 的生命周期并做相應的處理。而在 Flutter 中,我們可以利用**WidgetsBindingObserver**類,來實現同樣的需求。
接下來,我們就來看看具體如何實現這樣的需求。
首先,我們來看看 WidgetsBindingObserver 中具體有哪些回調函數:
~~~
abstract class WidgetsBindingObserver {
// 頁面 pop
Future<bool> didPopRoute() => Future<bool>.value(false);
// 頁面 push
Future<bool> didPushRoute(String route) => Future<bool>.value(false);
// 系統窗口相關改變回調,如旋轉
void didChangeMetrics() { }
// 文本縮放系數變化
void didChangeTextScaleFactor() { }
// 系統亮度變化
void didChangePlatformBrightness() { }
// 本地化語言變化
void didChangeLocales(List<Locale> locale) { }
//App 生命周期變化
void didChangeAppLifecycleState(AppLifecycleState state) { }
// 內存警告回調
void didHaveMemoryPressure() { }
//Accessibility 相關特性回調
void didChangeAccessibilityFeatures() {}
}
~~~
可以看到,WidgetsBindingObserver 這個類提供的回調函數非常豐富,常見的屏幕旋轉、屏幕亮度、語言變化、內存警告都可以通過這個實現進行回調。我們通過給 WidgetsBinding 的單例對象設置監聽器,就可以監聽對應的回調方法。
考慮到其他的回調相對簡單,你可以參考[官方文檔](https://api.flutter.dev/flutter/widgets/WidgetsBindingObserver-class.html),對照著進行練習。因此,我今天主要和你分享 App 生命周期的回調 didChangeAppLifecycleState,和幀繪制回調 addPostFrameCallback 與 addPersistentFrameCallback。
### 生命周期回調
didChangeAppLifecycleState 回調函數中,有一個參數類型為 AppLifecycleState 的枚舉類,這個枚舉類是 Flutter 對 App 生命周期狀態的封裝。它的常用狀態包括 resumed、inactive、paused 這三個。
* resumed:可見的,并能響應用戶的輸入。
* inactive:處在不活動狀態,無法處理用戶響應。
* paused:不可見并不能響應用戶的輸入,但是在后臺繼續活動中。
這里,我來和你分享一個實際案例。
在下面的代碼中,我們在 initState 時注冊了監聽器,在 didChangeAppLifecycleState 回調方法中打印了當前的 App 狀態,最后在 dispose 時把監聽器移除:
~~~
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver{// 這里你可以再回顧下,第 7 篇文章“函數、類與運算符:Dart 是如何處理信息的?”中關于 Mixin 的內容
...
@override
@mustCallSuper
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);// 注冊監聽器
}
@override
@mustCallSuper
void dispose(){
super.dispose();
WidgetsBinding.instance.removeObserver(this);// 移除監聽器
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
print("$state");
if (state == AppLifecycleState.resumed) {
//do sth
}
}
}
~~~
我們試著切換一下前、后臺,觀察控制臺輸出的 App 狀態,可以發現:
* 從后臺切入前臺,控制臺打印的 App 生命周期變化如下: AppLifecycleState.paused->AppLifecycleState.inactive->AppLifecycleState.resumed;
* 從前臺退回后臺,控制臺打印的 App 生命周期變化則變成了:AppLifecycleState.resumed->AppLifecycleState.inactive->AppLifecycleState.paused。
可以看到,App 前后臺切換過程中打印出的狀態是完全符合預期的。
:-: 
圖 4 App 切換前后臺狀態變化示意
### 幀繪制回調
除了需要監聽 App 的生命周期回調做相應的處理之外,有時候我們還需要在組件渲染之后做一些與顯示安全相關的操作。
在 iOS 開發中,我們可以通過 dispatch\_async(dispatch\_get\_main\_queue(),^{…}) 方法,讓操作在下一個 RunLoop 執行;而在 Android 開發中,我們可以通過 View.post() 插入消息隊列,來保證在組件渲染后進行相關操作。
其實,**在 Flutter 中實現同樣的需求會更簡單**:依然使用萬能的 WidgetsBinding 來實現。
WidgetsBinding 提供了單次 Frame 繪制回調,以及實時 Frame 繪制回調兩種機制,來分別滿足不同的需求:
* 單次 Frame 繪制回調,通過 addPostFrameCallback 實現。它會在當前 Frame 繪制完成后進行進行回調,并且只會回調一次,如果要再次監聽則需要再設置一次。
~~~
WidgetsBinding.instance.addPostFrameCallback((_){
print(" 單次 Frame 繪制回調 ");// 只回調一次
});
~~~
* 實時 Frame 繪制回調,則通過 addPersistentFrameCallback 實現。這個函數會在每次繪制 Frame 結束后進行回調,可以用做 FPS 監測。
~~~
WidgetsBinding.instance.addPersistentFrameCallback((_){
print(" 實時 Frame 繪制回調 ");// 每幀都回調
});
~~~
## 總結
在今天這篇文章中,我和你介紹了 State 和 App 的生命周期,這是 Flutter 給我們提供的,感知 Widget 和應用在不同階段狀態變化的回調。
首先,我帶你重新認識了 Widget 生命周期的實際承載者 State。我將 State 的生命周期劃分為了創建(插入視圖樹)、更新(在視圖樹中存在)、銷毀(從視圖樹種移除)這 3 個階段,并為你介紹了每個階段中涉及的關鍵方法,希望你能夠深刻理解 Flutter 組件從加載到卸載的完整周期。
然后,通過與原生 Android、iOS 平臺能力的對比,以及查看 WidgetsBindingObserver 源碼的方式,我與你講述了 Flutter 常用的生命周期狀態切換機制。希望你能掌握 Flutter 的 App 生命周期監聽方法,并理解 Flutter 常用的生命周期狀態切換機制。
最后,我和你一起學習了 Flutter 幀繪制回調機制,理解了單次 Frame 繪制回調與實時 Frame 繪制回調的異同與使用場景。
為了能夠精確地控制 Widget,Flutter 提供了很多狀態回調,所以今天這一篇文章,涉及到的方法有些多。但,**只要你分別記住創建、更新與銷毀這三條主線的調用規則,就一定能把這些方法的調用順序串起來,并能在實際開發中運用正確的方法去感知狀態變更,寫出合理的組件。**
我把今天分享所涉及的全部知識點打包成了一個[小項目](https://github.com/cyndibaby905/11_Flutter_lifecycle),你可以下載后在工程中實際運行,并對照著今天的課程學習,體會在不同場景下這些函數的調用時機。
## 思考題
最后,請你思考下這兩個問題:
1. 構造方法與 initState 函數在 State 的生命周期中都只會被調用一次,也大都用于完成一些初始化的工作。根據我們今天的學習,你能否舉出例子,比如哪些操作適合放在構造方法,哪些操作適合放在 initState,而哪些操作必須放在 initState。
2. 通過 didChangeDependencies 觸發 Widget 重建時,父子 Widget 之間的生命周期函數調用時序是怎樣的?
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略