Flutter–Flutter中Widget、App的生命周期
前言
在App的開發過程中,我們通常都需要了解App以及各個頁面的生命周期,方便我們在App進入前台時啟動一些任務,在進入後台後暫停一些任務。同時,各個頁面的生命周期也很重要,每個頁面消失時要做一些記憶體清理、計時器清除、通知清除等操作。所以,本文主要就是學習一下在flutter開發App的時候,如何去懟App以及各個頁面的生命周期進行監聽和回調。
一、頁面的生命周期
在Flutter開發中,所有的組件和頁面都繼承自Widget,所以探索頁面的生命周期其實就是Widget的生命周期。和Android的Activity和iOS的Controller一樣,在Widget中,也有對應生命周期的一些方法函數。當進行到某一階段時,會自動回調對應的方法函數。
在 Flutter 中一切皆 組件,而組件又分為 StatefulWidget(有狀態) 和 StatelessWidget(無狀態)組件 ,他們之間的區別是 StatelessWidget 組件發生變化時必須重新創建新的實例,而 StatefulWidget 組件則可以直接改變當前組件的狀態而無需重新創建新的實例。
- StatelessWidget是無狀態組件,頁面一旦生成是不會產生變化的,所以只有createElement和build的生命周期
- StatefulWidget是有狀態組件,在頁面中可以進行刷新等操作,所以該組件的狀態變化會更多一些,分為初始化階段、更新階段以及銷毀階段等
1.1 StatefulWidget生命周期概述
下面就主要以分析一下StatefulWidget有狀態組件的生命周期。 下圖是StatefulWidget 和 State 結構圖是StatefulWidget 組件生命周期的概覽,不同版本的差異也可以對比此結構圖。
其生命周期流程圖則如下所示,下圖中所有方框都是StatefulWidget中可以重寫的方法,這些方法在響應的生命周期狀態下會被自動回調。
1.2 StatefulWidget生命周期詳細分析
1.2.1 生命周期一:createState
下面是一個非常簡單的 StatefulWidget 組件:
class StatefulWidgetDemo extends StatefulWidget { @override _StatefulWidgetDemoState createState() => _StatefulWidgetDemoState(); } class _StatefulWidgetDemoState extends State<StatefulWidgetDemo> { @override Widget build(BuildContext context) { return Container(); } }
當我們構建一個 StatefulWidget 組件時,首先執行其構造函數(上面的程式碼沒有顯示的構造函數,但有默認的無參構造函數),然後執行 createState 函數。但構造函數並不是生命周期的一部分。當 StatefulWidget 組件插入到組件樹中時 createState 函數由 Framework 調用,此函數在樹中給定的位置為此組件創建 State,如果在組件樹的不同位置都插入了此組件,即創建了多個此組件,如下:
Row(children: [
MyStatefulWidget(),
MyStatefulWidget(),
MyStatefulWidget(),
],)
那麼系統會為每一個組件創建一個單獨的 State,當組件從組件樹中移除,然後重新插入到組件樹中時, createState 函數將會被調用創建一個新的 State。
createState 函數執行完畢後表示當前組件已經在組件樹中,此時有一個非常重要的屬性 mounted被 Framework 設置為 true。
1.2.2 生命周期二:initState
initState 函數在組件被插入樹中時被 Framework 調用(在 createState 之後),此函數只會被調用一次,子類通常會重寫此方法,在其中進行初始化操作,比如載入網路數據,重寫此方法時一定要調用 super.initState(),如下:
@override void initState() { super.initState(); //初始化... }
如果此組件需要訂閱通知,比如 ChangeNotifier 或者 Stream,則需要在不同的生命周期內正確處理訂閱和取消訂閱通知。
- 在 initState 中訂閱通知。
- 在 didUpdateWidget 中,如果需要替換舊組件,則在舊對象中取消訂閱,並在新對象中訂閱通知。
- 並在 dispose 中取消訂閱。
另外在此函數中不能調用 BuildContext.dependOnInheritedWidgetOfExactType,典型的錯誤寫法如下:
@override void initState() { super.initState(); IconTheme iconTheme = context.dependOnInheritedWidgetOfExactType<IconTheme>(); }
異常資訊如下:
解決方案:
@override void didChangeDependencies() { super.didChangeDependencies(); context.dependOnInheritedWidgetOfExactType<IconTheme>(); }
上面的用法作為初學者使用的比較少,但下面的錯誤程式碼大部分應該都寫過:
@override void initState() { super.initState(); showDialog(context: context,builder: (context){ return AlertDialog(); }); }
異常資訊如下:
解決方案:
@override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { showDialog(context: context,builder: (context){ return AlertDialog(title: Text('AlertDialog'),); }); }); }
注意:彈出 AlertDialog 在 didChangeDependencies 中調用也會出現異常,但和上面的異常不是同一個。
1.2.3 生命周期三:didChangeDependencies
didChangeDependencies 方法在 initState 之後由 Framework 立即調用。另外,當此 State 對象的依賴項更改時被調用,比如其所依賴的 InheritedWidget 發生變化時, Framework 會調用此方法通知組件發生變化。
此方法是生命周期中第一個可以使用 BuildContext.dependOnInheritedWidgetOfExactType 的方法,此方法很少會被重寫,因為 Framework 會在依賴發生變化時調用 build,需要重寫此方法的場景是:依賴發生變化時需要做一些耗時任務,比如網路請求數據。
didChangeDependencies 方法調用後,組件的狀態變為 dirty,立即調用 build 方法。
1.2.4 生命周期四:build
此方法是我們最熟悉的,在方法中創建各種組件,繪製到螢幕上。 Framework會在多種情況下調用此方法:
- 調用 initState 方法後。
- 調用 didUpdateWidget 方法後。
- 收到對 setState 的調用後。
- 此 State 對象的依存關係發生更改後(例如,依賴的 InheritedWidget 發生了更改)。
- 調用 deactivate 之後,然後將 State 對象重新插入樹的另一個位置。
此方法可以在每一幀中調用,此方法中應該只包含構建組件的程式碼,不應該包含其他額外的功能,尤其是耗時任務。
1.2.5 生命周期五:didUpdateWidget
當組件的 configuration 發生變化時調用此函數,當父組件使用相同的 runtimeType 和 Widget.key 重新構建一個新的組件時,Framework 將更新此 State 對象的組件屬性以引用新的組件,然後使用先前的組件作為參數調用此方法。
@override void didUpdateWidget(covariant StatefulLifecycle oldWidget) { super.didUpdateWidget(oldWidget); print('didUpdateWidget'); }
此方法中通常會用當前組件與前組件進行對比。Framework 調用完此方法後,會將組件設置為 dirty 狀態,然後調用 build 方法,因此無需在此方法中調用 setState 方法。
1.2.6 生命周期六:deactivate
當框架從樹中移除此 State 對象時將會調用此方法,在某些情況下,框架將重新插入 State 對象到樹的其他位置(例如,如果包含該樹的子樹 State 對象從樹中的一個位置移植到另一位置),框架將會調用 build 方法來提供 State 對象適應其在樹中的新位置。
1.2.7 生命周期七:dispose
當框架從樹中永久移除此 State 對象時將會調用此方法,與 deactivate 的區別是,deactivate 還可以重新插入到樹中,而 dispose 表示此 State 對象永遠不會在 build。調用完 dispose後,mounted 屬性被設置為 false,也代表組件生命周期的結束,此時再調用 setState 方法將會拋出異常。
子類重寫此方法,釋放相關資源,比如動畫等。
1.3 非常重要的幾個概念
下面介紹幾個非常重要的概念和方法,這些並不是生命周期的一部分,但是生命周期過程中的產物,與生命周期關係非常緊密。
1.3.1 mounted
mounted 是 State 對象中的一個屬性,此屬性表示當前組件是否在樹中,在創建 State 之後,調用 initState 之前,Framework 會將 State 和 BuildContext 進行關聯,當 Framework 調用 dispose時,mounted 被設置為 false,表示當前組件已經不在樹中。
createState 函數執行完畢後表示當前組件已經在組件樹中,屬性 mounted 被 Framework 設置為 true,平時寫程式碼時或者看其他開源程式碼時經常看到如下程式碼:
if(mounted){ setState(() { ... }); }
強烈建議:在調用 setState 時加上 mounted 判斷。
為什麼要加上如此判斷?因為如果當前組件未插入到樹中或者已經從樹中移除時,調用 setState 會拋出異常,加上 mounted 判斷,則表示當前組件在樹中。
1.3.2 dirty 和 clean
dirty 表示組件當前的狀態為 臟狀態,下一幀時將會執行 build 函數,調用 setState 方法或者 執行 didUpdateWidget 方法後,組件的狀態為 dirty。
clean 與 dirty 相對應,clean 表示組件當前的狀態為 乾淨狀態,clean 狀態下組件不會執行 build函數。
1.3.3 setState
setState 方法是開發者經常調用的方法,此方法調用後,組件的狀態變為 dirty,當有數據要更新時,調用此方法。
1.3.4 reassemble
reassemble 用於開發,比如 hot reload ,在 release 版本中不會回調此方法。
二、App的生命周期
App的生命周期與上面所說的StatefulWidget 組件的生命周期是不同的,這裡App的生命周期指的是特定平台相關操作所產生的生命周期,比如 Android 中 App 退到後台後的onPause等。App正在播放影片,此時回到手機桌面或者切換到其他App,那麼此時影片應該暫停播放。
2.1 App的生命周期監聽實現
App的生命周期的監聽,在Flutter中需要通過監聽器WidgetsBindingObserver監聽器中的AppLifecycleState方法來是實現。
class CEAppLifePage extends StatefulWidget { CEAppLifePage({Key key}) : super(key: key); @override _CEAppLifePageState createState() => _CEAppLifePageState(); } //實現WidgetsBindingObserver觀察者 class _CEAppLifePageState extends State<CEAppLifePage> with WidgetsBindingObserver{ @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); //添加觀察者 } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("App生命周期"),), body: Column(children: <Widget>[ Text('test data 1'), RaisedButton( child: Text('next page'), onPressed: (){ // KLEasyLoading.showInfo('生命周期 test'); Navigator.of(context).push(new MaterialPageRoute(builder: (context) => CELoginPage())); } ), ],), ); } // 生命周期變化時回調 // resumed:應用可見並可響應用戶操作,app進入前台 // inactive:用戶可見,但不可響應用戶操作,比如來了個電話,前後台切換的過渡狀態 // paused:已經暫停了,用戶不可見、不可操作,app進入後台 // suspending:應用被掛起,此狀態IOS永遠不會回調 @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); print("didChangeAppLifecycleState: $state"); } //當前系統改變了一些訪問性活動的回調 @override void didChangeAccessibilityFeatures() { super.didChangeAccessibilityFeatures(); print("didChangeAccessibilityFeatures"); } //低記憶體回調 @override void didHaveMemoryPressure() { super.didHaveMemoryPressure(); print("didHaveMemoryPressure"); } //用戶本地設置變化時調用,如系統語言改變 @override void didChangeLocales(List<Locale> locale) { super.didChangeLocales(locale); print("didChangeLocales"); } //應用尺寸改變時回調,例如旋轉 @override void didChangeMetrics() { super.didChangeMetrics(); Size size = WidgetsBinding.instance.window.physicalSize; print("didChangeMetrics :寬:${size.width} 高:${size.height}"); } //系統切換主題時回調 @override void didChangePlatformBrightness() { super.didChangePlatformBrightness(); print("didChangePlatformBrightness"); } ///文字係數變化 @override void didChangeTextScaleFactor() { super.didChangeTextScaleFactor(); print( "didChangeTextScaleFactor :${WidgetsBinding.instance.window.textScaleFactor}"); } @override void dispose() { super.dispose(); WidgetsBinding.instance.removeObserver(this); //銷毀觀察者 } }
重點是重寫 didChangeAppLifecycleState 方法,AppLifecycleState 中的狀態包括:resumed、inactive、paused、detached。
didChangeAppLifecycleState 方法的回調來源於系統的通知(notifications),正常情況下,App是能正常接收到這些通知,但有的情況下是無法接收到通知的,比如用戶強制關機、手機沒有電自動關機等。
下面對其狀態詳細說明:
- resumed:應用程式可見且響應用戶輸入。
- inactive:應用程式處於非激活狀態,無法響應用戶輸入。在iOS上,打電話、響應TouchID請求、進入應用程式切換器或控制中心都處於此狀態。在Android上,分屏應用,打電話,彈出系統對話框或其他窗口等。
- pause:應用程式不可見且無法響應用戶輸入,運行在後台。處於此狀態時,引擎將不會調用 Window.onBeginFrame 和 Window.onDrawFrame。
- detached:應用程式仍寄存在Flutter引擎上,但與平台 View 分離。處於此狀態的時機:引擎首次載入到附加到一個平台 View的過程中,或者由於執行 Navigator pop ,view 被銷毀。
2.2 App生命周期中的常見問題
2.2.1 有2個頁面A和B,在B頁面點擊返回鍵返回到A,didChangeAppLifecycleState 不回調
其實這個問題大部分人是想要實現類似於Android 中 onResume 中的功能,用 didChangeAppLifecycleState 是無法實現此功能的,didChangeAppLifecycleState 是對應於整個應用程式的,而不是 Flutter 中 不同的路由(頁面)。
從A->B,在從B返回A,A重新載入數據使用如下方法:
// A頁面程式碼 class A extends StatelessWidget { @override Widget build(BuildContext context) { return RaisedButton(onPressed: ()async{ var result = await Navigator.of(context).push(MaterialPageRoute(builder: (context){ return B(); })); //從B返回到A時,執行下面的程式碼 //TODO 載入數據 }); } }
//B頁面程式碼: class B extends StatelessWidget { @override Widget build(BuildContext context) { return RaisedButton(onPressed: (){ Navigator.of(context).pop('返回的參數'); }); } }