面試官:知道 Flutter 生命週期?下週來入職!

語言: CN / TW / HK

公眾號名片 作者名片

2022 年什麼會火?什麼該學?本文正在參與“聊聊 2022 技術趨勢”徵文活動 」

作為一名移動端開發工程師,剛接觸 Flutter 的時候,一定會有這樣的疑問:Flutter 的生命週期是怎麼樣的?是如何處理生命週期的?我的 onCreate()[Android] 在哪裡?viewDidLoad()[iOS] 呢? 我的業務邏輯應該放在哪裡處理?初始化資料呢?希望看了這篇文章後,可以對你有一點小小的幫助。

安卓

如果你是一名安卓開發工程師,那麼對於 Activity 生命週期肯定不陌生

  • onCreate
  • onStart
  • onResume
  • onPause
  • onStop
  • onDestroy

android_life_cycle

iOS

如果你是一名 iOS 開發工程師,那麼 UIViewController 的生命週期肯定也已經很瞭解了。

  • viewDidLoad
  • viewWillAppear
  • viewDidAppear
  • viewWillDisappear
  • viewDidDisappear
  • viewDidUnload

ios_life_cycle

Flutter

知道了 Android 和 iOS 的生命週期,那麼 Flutter 呢?有和移動端對應的生命週期函式麼?如果之前你對 Flutter 有一點點了解的話,你會發現 Flutter 中有兩個主要的 Widget:StatelessWidget(無狀態)StatefulWidget(有狀態)。本篇文章我們主要來介紹下 StatefulWidget,因為它有著和 Android 和 iOS 相似的生命週期。

StatelessWidget

無狀態元件是不可變的,這意味著它們的屬性不能變化,所有的值都是最終的。可以理解為將外部傳入的資料轉化為介面展示的內容,只會渲染一次。 對於無狀態元件生命週期只有 build 這個過程。無狀態元件的構建方法通常只在三種情況下會被呼叫:小元件第一次被插入樹中,小元件的父元件改變其配置,以及它所依賴的 InheritedWidget 發生變化時。

StatefulWidget

有狀態元件持有的狀態可能在 Widget 生命週期中發生變化,是定義互動邏輯和業務邏輯。可以理解為具有動態可互動的內容介面,會根據資料的變化進行多次渲染。實現一個 StatefulWidget 至少需要兩個類:

  • 一個是 StatefulWidget 類。
  • 另一個是 Sate 類。StatefulWidget 類本身是不可變的,但是 State 類在 Widget 生命週期中始終存在。StatefulWidget 將其可變的狀態儲存在由 createState 方法建立的 State 物件中,或者儲存在該 State 訂閱的物件中。

StatefulWidget 生命週期

  • createState:該函式為 StatefulWidget 中建立 State 的方法,當 StatefulWidget 被建立時會立即執行 createState。createState 函式執行完畢後表示當前元件已經在 Widget 樹中,此時有一個非常重要的屬性 mounted 被置為 true。
  • initState:該函式為 State 初始化呼叫,只會被呼叫一次,因此,通常會在該回調中做一些一次性的操作,如執行 State 各變數的初始賦值、訂閱子樹的事件通知、與服務端互動,獲取服務端資料後呼叫 setState 來設定 State。
  • didChangeDependencies:該函式是在該元件依賴的 State 發生變化時會被呼叫。這裡說的 State 為全域性 State,例如系統語言 Locale 或者應用主題等,Flutter 框架會通知 widget 呼叫此回撥。類似於前端 Redux 儲存的 State。該方法呼叫後,元件的狀態變為 dirty,立即呼叫 build 方法。
  • build:主要是返回需要渲染的 Widget,由於 build 會被呼叫多次,因此在該函式中只能做返回 Widget 相關邏輯,避免因為執行多次而導致狀態異常。
  • reassemble:主要在開發階段使用,在 debug 模式下,每次熱過載都會呼叫該函式,因此在 debug 階段可以在此期間增加一些 debug 程式碼,來檢查程式碼問題。此回撥在 release 模式下永遠不會被呼叫。
  • didUpdateWidget:該函式主要是在元件重新構建,比如說熱過載,父元件發生 build 的情況下,子元件該方法才會被呼叫,其次該方法呼叫之後一定會再呼叫本元件中的 build 方法。
  • deactivate:在元件被移除節點後會被呼叫,如果該元件被移除節點,然後未被插入到其他節點時,則會繼續呼叫 dispose 永久移除。
  • dispose:永久移除元件,並釋放元件資源。呼叫完 dispose 後,mounted 屬性被設定為 false,也代表元件生命週期的結束。

不是生命週期但是卻非常重要的幾個概念

下面這些並不是生命週期的一部分,但是在生命週期中起到了很重要的作用。

  • mounted:是 State 中的一個重要屬性,相當於一個標識,用來表示當前元件是否在樹中。在 createState 後 initState 前,mounted 會被置為 true,表示當前元件已經在樹中。呼叫 dispose 時,mounted 被置為 false,表示當前元件不在樹中。
  • dirty:表示當前元件為髒狀態,下一幀時將會執行 build 函式,呼叫 setState 方法或者執行 didUpdateWidget 方法後,元件的狀態為 dirty。
  • clean:與 dirty 相對應,clean 表示元件當前的狀態為乾淨狀態,clean 狀態下元件不會執行 build 函式。

stateful_widget_lifecycle 生命週期流程圖

上圖為 flutter 生命週期流程圖

大致分為四個階段

  1. 初始化階段,包括兩個生命週期函式 createState 和 initState;
  2. 元件建立階段,包括 didChangeDependencies 和 build;
  3. 觸發元件多次 build ,這個階段有可能是因為 didChangeDependencies、 setState 或者 didUpdateWidget 而引發的元件重新 build ,在元件執行過程中會多次觸發,這也是優化過程中需要著重注意的點;
  4. 最後是元件銷燬階段,deactivate 和 dispose。

元件首次載入執行過程

首先我們來實現下面這段程式碼(類似於 flutter 自己的計數器專案),康康元件首次建立是否按照上述流程圖中的順序來執行的。

  1. 建立一個 flutter 專案;
  2. 建立 count_widget.dart 中新增以下程式碼;

``` import 'package:flutter/material.dart';

class CountWidget extends StatefulWidget { CountWidget({Key key}) : super(key: key);

@override _CountWidgetState createState() { print('count createState'); return _CountWidgetState(); } }

class _CountWidgetState extends State { int _count = 0; void _incrementCounter() { setState(() { print('count setState'); _count++; }); }

@override void initState() { print('count initState'); super.initState(); }

@override void didChangeDependencies() { print('count didChangeDependencies'); super.didChangeDependencies(); }

@override void didUpdateWidget(CountWidget oldWidget) { print('count didUpdateWidget'); super.didUpdateWidget(oldWidget); }

@override void deactivate() { print('count deactivate'); super.deactivate(); }

@override void dispose() { print('count dispose'); super.dispose(); }

@override void reassemble() { print('count reassemble'); super.reassemble(); }

@override Widget build(BuildContext context) { print('count build'); return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '$_count', style: Theme.of(context).textTheme.headline4, ), Padding( padding: EdgeInsets.only(top: 100), child: IconButton( icon: Icon( Icons.add, size: 30, ), onPressed: _incrementCounter, ), ), ], ), ); } } ```

上述程式碼把 StatefulWidget 的一些生命週期都進行了重寫,並且在執行中都列印了標識,方便看到函式的執行順序。

  1. 在 main.dart 中載入該元件。程式碼如下:

``` import 'package:flutter/material.dart';

import './pages/count_widget.dart';

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(title: 'Flutter Demo Home Page'), ); } }

class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override _MyHomePageState createState() { return _MyHomePageState(); } }

class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: CountWidget(), ); } } ```

這時 CountWidget 作為 MyHomePage 的子元件。我們開啟模擬器,開始執行。在控制檯可以看到如下日誌,可以看出 StatefulWidget 在第一次被建立的時候是呼叫下面四個函式。

flutter: count createState flutter: count initState flutter: count didChangeDependencies flutter: count build

點選螢幕上的 ➕ 按鈕,_count 增加 1,模擬器上的數字由 0 變為 1,日誌如下。也就是說在狀態發生變化的時候,會呼叫 setStatebuild 兩個函式。

flutter: count setState flutter: count build

command + s 熱過載後,日誌如下:

flutter: count reassemble flutter: count didUpdateWidget flutter: count build

註釋掉 main.dart 中的 CountWidget,command + s 熱過載後,這時 CountWidget 消失在模擬器上,日誌如下:

class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), // body: CountWidget(), ); } }

flutter: count reassemble flutter: count deactivate flutter: count dispose

經過上述一系列操作之後,通過日誌列印並結合生命週期流程圖,我們可以很清晰的看出各生命週期函式的作用以及理解生命週期的幾個階段。 相信很多細心的同學已經發現了一個細節,那就是 build 方法在不同的操作中都被呼叫了,下面我們來介紹什麼情況下會觸發元件再次 build。

觸發元件再次 build

觸發元件再次 build 的方式有三種,分別是 setStatedidChangeDependenciesdidUpdateWidget

1.setState 很好理解,只要元件狀態發生變化時,就會觸發元件 build。在上述的操作過程中,點選 ➕ 按鈕,_count 會加 1,結果如下圖:

set_state

2.didChangeDependencies,元件依賴的全域性 state 發生了變化時,也會呼叫 build。例如系統語言等、主題色等。

3.didUpdateWidget,我們以下方程式碼為例。在 main.dart 中,同樣的重寫生命週期函式,並列印。在 CountWidget 外包一層 Column ,並建立同級的 RaisedButton 做為父 Widget 中的計數器。

``` class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override _MyHomePageState createState() { print('main createState'); return _MyHomePageState(); } }

class _MyHomePageState extends State { int mainCount = 0;

void _changeMainCount() { setState(() { print('main setState'); mainCount++; }); }

@override void initState() { print('main initState'); super.initState(); }

@override void didChangeDependencies() { print('main didChangeDependencies'); super.didChangeDependencies(); }

@override void didUpdateWidget(MyHomePage oldWidget) { print('main didUpdateWidget'); super.didUpdateWidget(oldWidget); }

@override void deactivate() { print('main deactivate'); super.deactivate(); }

@override void dispose() { print('main dispose'); super.dispose(); }

@override void reassemble() { print('main reassemble'); super.reassemble(); }

@override Widget build(BuildContext context) { print('main build'); return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Column( children: [ RaisedButton( onPressed: () => _changeMainCount(), child: Text('mainCount = $mainCount'), ), CountWidget(), ], ), ); } } ```

重新載入 app,可以看到列印日誌如下:

father_widget_create_state

flutter: main createState flutter: main initState flutter: main didChangeDependencies flutter: main build flutter: count createState flutter: count initState flutter: count didChangeDependencies flutter: count build

可以發現: * 父元件也經歷了 createStateinitStatedidChangeDependenciesbuild 這四個過程。 * 並且父元件要在 build 之後才會建立子元件。

點選 MyHomePage(父元件)的 mainCount 按鈕 ,列印如下:

flutter: main setState flutter: main build flutter: count didUpdateWidget flutter: count build

點選 CountWidget 的 ➕ 按鈕,列印如下:

flutter: count setState flutter: count build

可以說明父元件的 State 變化會引起子元件的 didUpdateWidget 和 build,子元件自己的狀態變化不會引起父元件的狀態改變

元件銷燬

我們重複上面的操作,為 CountWidget 新增一個子元件 CountSubWidget,並用 count sub 字首列印日誌。重新載入 app。

註釋掉 CountWidget 中的 CountSubWidget,列印日誌如下:

flutter: main reassemble flutter: count reassemble flutter: count sub reassemble flutter: main didUpdateWidget flutter: main build flutter: count didUpdateWidget flutter: count build flutter: count sub deactivate flutter: count sub dispose

恢復到註釋前,註釋掉 MyHomePage 中的 CountWidget,列印如下: flutter: main reassemble flutter: count reassemble flutter: count sub reassemble flutter: main didUpdateWidget flutter: main build flutter: count deactivate flutter: count sub deactivate flutter: count sub dispose flutter: count dispose

因為是熱過載,所以會呼叫 reassembledidUpdateWidgetbuild,我們可以忽略帶有這幾個函式的列印日誌。可以得出結論: 父元件移除,會先移除節點,然後子元件移除節點,子元件被永久移除,最後是父元件被永久移除。

Flutter App Lifecycle

上面我們介紹的生命週期主要是 StatefulWidget 元件的生命週期,下面我們來簡單介紹一下和 app 平臺相關的生命週期,比如退出到後臺。

我們建立 app_lifecycle_state.dart 檔案並建立 AppLifecycle,他是一個 StatefulWidget,但是他要繼承 WidgetsBindingObserver。

``` import 'package:flutter/material.dart';

class AppLifecycle extends StatefulWidget { AppLifecycle({Key key}) : super(key: key);

@override _AppLifecycleState createState() { print('sub createState'); return _AppLifecycleState(); } }

class _AppLifecycleState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); print('sub initState'); }

@override void didChangeAppLifecycleState(AppLifecycleState state) { // TODO: implement didChangeAppLifecycleState super.didChangeAppLifecycleState(state); print('didChangeAppLifecycleState'); if (state == AppLifecycleState.resumed) { print('resumed:'); } else if (state == AppLifecycleState.inactive) { print('inactive'); } else if (state == AppLifecycleState.paused) { print('paused'); } else if (state == AppLifecycleState.detached) { print('detached'); } }

@override Widget build(BuildContext context) { print('sub build'); return Container( child: Text('data'), ); } } ```

didChangeAppLifecycleState 方法是重點,AppLifecycleState 中的狀態包括:resumedinactivepauseddetached 四種。

didChangeAppLifecycleState 方法的依賴於系統的通知(notifications),正常情況下,App是可以接收到這些通知,但有個別情況下是無法接收到通知的,比如使用者關機等。它的四種生命週期狀態列舉原始碼中有詳細的介紹和說明,下面附上原始碼以及簡單的翻譯說明。

app_life_cycle_state

  • resumed:該應用程式是可見的,並對使用者的輸入作出反應。也就是應用程式進入前臺。
  • inactive:應用程式處於非活動狀態,沒有接收使用者的輸入。在 iOS 上,這種狀態對應的是應用程式或 Flutter 主機檢視在前臺非活動狀態下執行。當處於電話呼叫、響應 TouchID 請求、進入應用切換器或控制中心時,或者當 UIViewController 託管的 Flutter 應用程式正在過渡。在 Android 上,這相當於應用程式或 Flutter 主機檢視在前臺非活動狀態下執行。當另一個活動被關注時,如分屏應用、電話呼叫、畫中畫應用、系統對話方塊或其他視窗,應用會過渡到這種狀態。也就是應用進入後臺。
  • pause:該應用程式目前對使用者不可見,對使用者的輸入沒有反應,並且在後臺執行。當應用程式處於這種狀態時,引擎將不會呼叫。也就是說應用進入非活動狀態。
  • detached:應用程式仍然被託管在flutter引擎上,但與任何主機檢視分離。處於此狀態的時機:引擎首次載入到附加到一個平臺 View 的過程中,或者由於執行 Navigator pop,view 被銷燬。

除了 app 生命週期的方法,Flutter 還有一些其他不屬於生命週期,但是也會在一些特殊時機被觀察到的方法,如 didChangeAccessibilityFeatures(當前系統改變了一些訪問性活動的回撥)didHaveMemoryPressure(低記憶體回撥)didChangeLocales(使用者本地設定變化時呼叫,如系統語言改變)didChangeTextScaleFactor(文字係數變化) 等,如果有興趣的話,可以去試一試。

總結

本篇文章主要介紹了 Widget 中的 StatefulWidget 的生命週期,以及 Flutter App 相關的生命週期。但是要切記,StatefulWidget 雖好,但也不要無腦的所有 Widget 全都用它,能使用 StatelessWidget 還是要儘量去使用 StatelessWidget(仔細想一下,這是為什麼呢?)。好啦,看完本篇文章,你就是 Flutter 初級開發工程師了,可以去面試了(狗頭保命)。

最後

真正堅持到最後的人,往往靠的不是短暫的激情,而是恰到好處的喜歡和投入。你還那麼年輕,完全可以成為任何你想要成為的樣子!

更多精彩請關注我們的公眾號「百瓶技術」,有不定期福利呦!