深入了解 Flutter 中 Widget 與 App 的生命周期及其使用

前言

在上篇文章中,了解到通過父 Widget 初始化時傳入的靜態(tài)配置,StatelessWidget 就能完全控制其靜態(tài)展示。而 StatefulWidget 需要借助于 State 對象,在特定的階段來處理用戶的交互或其內(nèi)部數(shù)據(jù)的變化,并體現(xiàn)在 UI 上。在這些特定的階段,就涵蓋了一個組件從加載到卸載的全過程,即生命周期。與 iOS 的 ViewController 和 Android 的 Activity 一樣,F(xiàn)lutter 中的 Widget 也存在生命周期,并且通過 State 來體現(xiàn)。

而 App 則是一個特殊的 Widget。除了需要處理視圖顯示的各個階段(視圖的生命周期),還需要應(yīng)對應(yīng)用從啟動到退 出所經(jīng)歷的各個狀態(tài)(App 的生命周期)。

下面針對 Widget(的 State)和 App 的兩個維度,去了解它們生命周期。


(一)State 生命周期

State 的生命周期,指的是在用戶參與的情況下,其關(guān)聯(lián)的 Widget 所經(jīng)歷的,從創(chuàng)建到顯示再到更新最后到停止,直至銷毀等各個過程階段。這些不同的階段涉及到特定的任務(wù)處理,因此為了寫出一個體驗和性能良好的控件,正確理解 State 的生命周期至關(guān)重要。

State 的生命周期流程,如圖所示:


State 的生命周期圖

State 的生命周期可以分為三個階段:創(chuàng)建(插入視圖樹)、更新(在視圖樹中存在)、銷毀(從視圖樹中移除)。

(1)創(chuàng)建

State 初始化的執(zhí)行順序為:構(gòu)造方法 > initState > didChangeDependencies > build,隨后完成頁面渲染。

  • 構(gòu)造方法是 State 生命周期的起點(diǎn),F(xiàn)lutter 會通過調(diào)用 StatefulWidget.creatState() 來創(chuàng)建一個 State。通過構(gòu)造方法,來接受父 Widget 傳遞的初始化 UI 配置數(shù)據(jù),決定 Widget 最初的呈現(xiàn)效果。
  • initState,會在 State 對象被插入視圖樹的時候調(diào)用。這個函數(shù)在 State 的生命周期中只會被調(diào)用一次,所以我們可以在這里做一些初始化工資,比如為狀態(tài)變量設(shè)定默認(rèn)值。
  • didChangeDependencies 用來專門處理 State 對象依賴關(guān)系變化,會在 initState() 調(diào)用結(jié)束后,被 Flutter 調(diào)用。
  • build,作用是構(gòu)建視圖,根據(jù)父 Widget 傳遞過來的初始化配置數(shù)據(jù),以及 State 的當(dāng)前狀態(tài),創(chuàng)建一個 Widget 然后返回。
(2)更新

Widget 的狀態(tài)更新,主要由三個方法觸發(fā):setState、didChangeDependencies 與 didUpdateWidget。一旦這三個方法被調(diào)用,F(xiàn)lutter 隨后就會銷毀老 Widget,并調(diào)用 build 方法重建 Widget。

  • setState:當(dāng)狀態(tài)數(shù)據(jù)發(fā)生變化時,通過調(diào)用這個方法通知 Flutter 更新重構(gòu) Widget。
  • didChangeDependencies:State 對象的依賴關(guān)系發(fā)生變化后,F(xiàn)lutter 會調(diào)用這個方法,隨后觸發(fā)組件構(gòu)建。哪些情況下 State 對象的依賴關(guān)系會發(fā)生變化呢?典型的場景是:系統(tǒng)語言 Locale 或應(yīng)用主題改變時,系統(tǒng)會通知 State 執(zhí)行 didChangeDependencies 回調(diào)方法。
  • didUpdateWidget:當(dāng) Widget 的配置發(fā)生變化時,比如,父 Widget 觸發(fā)重建(即父 Widget 的狀態(tài)發(fā)生變化時),熱重載時,系統(tǒng)會調(diào)用這個函數(shù)。
(3)銷毀

當(dāng)組件被移除,或是頁面被銷毀的時候,系統(tǒng)會調(diào)用 deactivate 和 dispose 這兩個方法,來移除或銷毀組件。

  • deactivate:當(dāng)組件的可見狀態(tài)發(fā)生變化時,deactivate 函數(shù)會被調(diào)用,這時 State 會被暫時從視圖樹中移除。當(dāng)頁面切換時,由于 State 對象在視圖樹中的位置發(fā)生了變化,需要先暫時移除后再重新添加,重新觸發(fā)組件構(gòu)建,因為這個函數(shù)也會被調(diào)用。
  • dispose:當(dāng) State 被永久地從視圖樹中移除時,F(xiàn)lutter 會調(diào)用 dispose 函數(shù)。而一旦到這個階段,組件就要被銷毀了,所以我們可以在這里進(jìn)行最終的資源釋放、移除監(jiān)聽、清理環(huán)境等等。

舉例說明,如圖所示:

幾種常見場景下 State 生命周期圖

第一個圖展示了當(dāng)父 Widget 狀態(tài)發(fā)生變化時,父子雙方共同的生命周期變化;
第二、三圖展示頁面切換時,兩個關(guān)聯(lián)的 Widget 的生命周期函數(shù)是如何響應(yīng)的。

從功能,調(diào)用時機(jī)和調(diào)用次數(shù)的維度總結(jié):

生命周期中的方法調(diào)用對比

當(dāng)然,也可以在代碼中打印各個函數(shù)的回調(diào),更好的觀察 State 的生命周期:

  @override
  void initState() {
    super.initState();
    print('調(diào)用了 + initState');
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('調(diào)用了 + didChangeDependencies');
  }

  @override
  void setState(VoidCallback fn) {
    super.setState(fn);
    print('調(diào)用了 + setState');
  }


  @override
  void didUpdateWidget(MyHomePage oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('調(diào)用了 + didUpdateWidget');
  }

  @override
  void deactivate() {
    super.deactivate();
    print('調(diào)用了 + deactivate');
  }

  @override
  void dispose() {
    super.dispose();
    print('調(diào)用了 + dispose');
  }

(二)App 生命周期

App 生命周期定義了 App 從啟動到退出的全過程,其回調(diào)機(jī)制能夠讓我們可以根據(jù) App 狀態(tài)選擇合適的時機(jī)做恰當(dāng)?shù)氖虑椤?/p>

在原生 Android、iOS 開發(fā)中,有時我們需要在對應(yīng)的 App 生命周期事件中做相應(yīng)處理,比如 App 從后臺進(jìn)入前臺、從前臺退到后臺,或是在 UI 繪制完成后做一些處理。

這樣的需求,在原生開發(fā)中,我們可以通過重寫 Activity、ViewController 生命周期回調(diào)方法,或是注冊應(yīng)用程序的相關(guān)通知,來監(jiān)聽 App 的生命周期并做相應(yīng)的處理。而在 Flutter 中,可以利用 WidgetsBindingObserver 類,來實(shí)現(xiàn)同樣的需求。

WidgetsBindingObserver 類中的回調(diào)函數(shù):

abstract class WidgetsBindingObserver {
  // 頁面 pop
  Future<bool> didPopRoute() => Future<bool>.value(false);
  // 頁面 push
  Future<bool> didPushRoute(String route) => Future<bool>.value(false);
  // 系統(tǒng)窗口相關(guān)改變回調(diào),如旋轉(zhuǎn)
  void didChangeMetrics() { }
  // 文本縮放系數(shù)變化
  void didChangeTextScaleFactor() { }
  // 系統(tǒng)亮度變化
  void didChangePlatformBrightness() { }
  // 本地化語言變化
  void didChangeLocales(List<Locale> locale) { }
  //App 生命周期變化
  void didChangeAppLifecycleState(AppLifecycleState state) { }
  // 內(nèi)存警告回調(diào)
  void didHaveMemoryPressure() { }
  //Accessibility 相關(guān)特性回調(diào)
  void didChangeAccessibilityFeatures() {}
}

可以看到,WidgetsBindingObserver 類中的回調(diào)函數(shù)非常豐富,常見的屏幕旋轉(zhuǎn)、屏幕亮度、語言變化、內(nèi)存警告等都可以通過這個實(shí)現(xiàn)進(jìn)行回調(diào)。我們通過給 WidgetsBindingObserver 設(shè)置監(jiān)聽器,就可以監(jiān)聽對應(yīng)的回到方法。

參考文檔

下面主要對 App 生命周期的回調(diào) didChangeAppLifecycleState,和幀繪制回調(diào) addPostFrameCallback 與 addPersistentFrameCallback 進(jìn)行學(xué)習(xí)。

(1)生命周期回調(diào)(didChangeAppLifecycleState)

didChangeAppLifecycleState 回調(diào)函數(shù)中,有一個參數(shù)類型為 AppLifecycleState 的枚舉類,這個枚舉類是 Flutter 對 App 生命周期狀態(tài)的封裝。它的常用狀態(tài)包括 resumed、inactive、paused 這三個。

  • resumed:可見的,并能響應(yīng)用戶的輸入。
  • inactive:處在不活動狀態(tài),無法處理用戶響應(yīng)。
  • paused:不可見并不能響應(yīng)用戶的輸入,但是在后臺繼續(xù)活動中。

在代碼中進(jìn)行打印:
initState 中注冊監(jiān)聽器,在 didChangeAppLifecycleState 回調(diào)方法中打印當(dāng)前 App 狀態(tài),最后在 dispose 中移除監(jiān)聽器。

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  ···
  @override
  void initState() {
    super.initState();
    print('調(diào)用了 + initState');
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('調(diào)用了 + didChangeDependencies');
  }

  @override
  void setState(VoidCallback fn) {
    super.setState(fn);
    print('調(diào)用了 + setState');
  }

  @override
  void didUpdateWidget(MyHomePage oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('調(diào)用了 + didUpdateWidget');
  }

  @override
  void deactivate() {
    super.deactivate();
    print('調(diào)用了 + deactivate');
  }

  @override
  void dispose() {
    super.dispose();
    print('調(diào)用了 + dispose');
    WidgetsBinding.instance.removeObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    print('當(dāng)前state為:$state');
  }
}

當(dāng)切換前、后臺時,控制臺輸入打印 App 狀態(tài)如下:

  • 從后臺切入前臺,控制臺打印的 App 生命周期變化如下: AppLifecycleState.paused > AppLifecycleState.inactive > AppLifecycleState.resumed;
  • 從前臺退回后臺,控制臺打印的 App 生命周期變化則變成了:AppLifecycleState.resumed > AppLifecycleState.inactive > AppLifecycleState.paused。


    App 切換前后臺狀態(tài)變化
(2)幀繪制回調(diào)(addPostFrameCallback 與 addPersistentFrameCallback)

除了需要監(jiān)聽 App 的生命周期回調(diào)做相應(yīng)的處理之外,有時候我們還需要在組件渲染之后做一些與顯示安全相關(guān)的操作。

在 iOS 開發(fā)中,我們可以通過 dispatch_async(dispatch_get_main_queue(),^{…}) 方法,讓操作在下一個 RunLoop 執(zhí)行;而在 Android 開發(fā)中,我們可以通過 View.post() 插入消息隊列,來保證在組件渲染后進(jìn)行相關(guān)操作。

其實(shí),在 Flutter 中實(shí)現(xiàn)同樣的需求會更簡單,依然使用萬能的 WidgetBinding 來實(shí)現(xiàn)。

WidgetBinding 提供了單次 Frame 繪制回調(diào),以及實(shí)時 Frame 繪制回調(diào)兩種機(jī)制,來分別滿足不同的需求:

  • 單次 Frame 繪制回調(diào),通過 addPostFrameCallback 實(shí)現(xiàn)。它會在當(dāng)前 Frame 繪制完成后進(jìn)行回調(diào),并且只會回調(diào)一次,如果要再次監(jiān)聽則需要再設(shè)置一次。
WidgetsBinding.instance.addPostFrameCallback((_){
    print(" 單次 Frame 繪制回調(diào) ");// 只回調(diào)一次
  });
  • 實(shí)時 Frame 繪制回調(diào),則通過 addPersistentFrameCallback 實(shí)現(xiàn)。這個函數(shù)會在每次繪制 Frame 結(jié)束后進(jìn)行回調(diào),可以用做 FPS 監(jiān)測。
WidgetsBinding.instance.addPersistentFrameCallback((_){
  print(" 實(shí)時 Frame 繪制回調(diào) ");// 每幀都回調(diào)
});

總結(jié)

學(xué)習(xí)了,Widget 生命周期的實(shí)際承載者是 State,State 的生命周期劃分為:創(chuàng)建(插入視圖樹)、更新(在視圖樹中存在)、銷毀(從視圖樹中移除)三個階段。

Flutter 中常用的生命周期狀態(tài)切換機(jī)制 WidgetBindingObserver 與 Flutter 幀繪制回調(diào)機(jī)制,單次 Frame 繪制回調(diào)與實(shí)時 Frame 繪制回調(diào)的異同與使用場景。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內(nèi)容