Flutter 的生命週期
Flutter 的生命週期分爲兩個部分,一個是 Flutter 本身的組件的生命週期,一個是平臺相關的生命週期。
Stateful 組件的生命週期
StatefulWidget 組件的生命週期時非常重要的知識點,就像 Android 中 Activity 的生命週期一樣,不僅在以後的工作中經常用到,面試也會經常被問到。
在 Flutter 中一切皆 「組件」,而組件又分爲 「StatefulWidget(有狀態)」 和 StatelessWidget(無狀態) 組件 ,他們之間的區別是 StatelessWidget 組件發生變化時必須重新創建新的實例,而 StatefulWidget 組件則可以直接改變當前組件的狀態而無需重新創建新的實例。
注意:使用的 Flutter 版本 和 Dart 版本如下:
Flutter 1.22.4 • channel stable • https://github.com/flutter/flutter.git Framework • revision 1aafb3a8b9 (6 weeks ago) • 2020-11-13 09:59:28 -0800 Engine • revision 2c956a31c0 Tools • Dart 2.10.4
不同的版本 StatefulWidget 組件的生命週期會有差異。
下面的 StatefulWidget 和 State 結構圖是 StatefulWidget 組件生命週期的概覽,不同版本的差異也可以對比此結構圖。
生命週期流程圖:
下面詳細介紹 StatefulWidget 組件的生命週期。
生命週期一: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」。
生命週期二: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 中調用也會出現異常,但和上面的異常不是同一個。
❞
生命週期三:didChangeDependencies
didChangeDependencies 方法在 initState 之後由 Framework 立即調用。另外,當此 「State」 對象的依賴項更改時被調用,比如其所依賴的 「InheritedWidget」 發生變化時, Framework 會調用此方法通知組件發生變化。
此方法是生命週期中第一個可以使用 「BuildContext.dependOnInheritedWidgetOfExactType」 的方法,此方法很少會被重寫,因爲 Framework 會在依賴發生變化時調用 「build」,需要重寫此方法的場景是:依賴發生變化時需要做一些耗時任務,比如網絡請求數據。
didChangeDependencies 方法調用後,組件的狀態變爲 「dirty」,立即調用 build 方法。
生命週期四:build
此方法是我們最熟悉的,在方法中創建各種組件,繪製到屏幕上。Framework 會在多種情況下調用此方法:
-
調用 「initState」 方法後。
-
調用 「didUpdateWidget」 方法後。
-
收到對 「setState」 的調用後。
-
此 「State」 對象的依存關係發生更改後(例如,依賴的 「InheritedWidget」 發生了更改)。
-
調用 「deactivate」 之後,然後將 「State」 對象重新插入樹的另一個位置。
此方法可以在每一幀中調用,此方法中應該只包含構建組件的代碼,不應該包含其他額外的功能,尤其是耗時任務。
生命週期五:didUpdateWidget
當組件的 「configuration」 發生變化時調用此函數,當父組件使用相同的 「runtimeType」 和 「Widget.key」 重新構建一個新的組件時,Framework 將更新此 「State」 對象的組件屬性以引用新的組件,然後使用先前的組件作爲參數調用此方法。
@override
void didUpdateWidget(covariant StatefulLifecycle oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
此方法中通常會用當前組件與前組件進行對比。Framework 調用完此方法後,會將組件設置爲 「dirty」 狀態,然後調用 「build」 方法,因此無需在此方法中調用 「setState」 方法。
生命週期六:deactivate
當框架從樹中移除此 State 對象時將會調用此方法,在某些情況下,框架將重新插入 State 對象到樹的其他位置(例如,如果包含該樹的子樹 State 對象從樹中的一個位置移植到另一位置),框架將會調用 build 方法來提供 State 對象適應其在樹中的新位置。
生命週期七:dispose
當框架從樹中永久移除此 State 對象時將會調用此方法,與 「deactivate」 的區別是,「deactivate」 還可以重新插入到樹中,而 「dispose」 表示此 State 對象永遠不會在 「build」。調用完 「dispose」後,「mounted」 屬性被設置爲 false,也代表組件生命週期的結束,此時再調用 「setState」 方法將會拋出異常。
子類重寫此方法,釋放相關資源,比如動畫等。
非常重要的幾個概念
下面介紹幾個非常重要的概念和方法,這些並不是生命週期的一部分,但是生命週期過程中的產物,與生命週期關係非常緊密。
mounted
「mounted」 是 State 對象中的一個屬性,此屬性表示當前組件是否在樹中,在創建 「State」 之後,調用 「initState」 之前,Framework 會將 「State」 和 「BuildContext」 進行關聯,當 Framework 調用 「dispose」 時,mounted 被設置爲 false,表示當前組件已經不在樹中。
「createState」 函數執行完畢後表示當前組件已經在組件樹中,屬性 「mounted」 被 「Framework」 設置爲 「true」,平時寫代碼時或者看其他開源代碼時經常看到如下代碼:
if(mounted){
setState(() {
...
});
}
❝
強烈建議:在調用 「setState」 時加上 mounted 判斷。
❞
爲什麼要加上如此判斷?因爲如果當前組件未插入到樹中或者已經從樹中移除時,調用 「setState」 會拋出異常,加上 「mounted」 判斷,則表示當前組件在樹中。
dirty 和 clean
「dirty」 表示組件當前的狀態爲 「髒狀態」,下一幀時將會執行 「build」 函數,調用 「setState」 方法或者 執行 「didUpdateWidget」 方法後,組件的狀態爲 「dirty」。
「clean」 與 「dirty」 相對應,「clean」 表示組件當前的狀態爲 「乾淨狀態」,「clean」 狀態下組件不會執行 「build」 函數。
setState
「setState」 方法是開發者經常調用的方法,此方法調用後,組件的狀態變爲 「dirty」,當有數據要更新時,調用此方法。
reassemble
「reassemble」 用於開發,比如 「hot reload」 ,在 release 版本中不會回調此方法。
與平臺相關的生命週期
此篇文章所說的生命週期與 StatefulWidget 組件的生命週期是不同的,這裏平臺相關的生命週期指的是特定平臺相關操作所產生的生命週期,比如 Android 中 App 退到後臺後的 onPause 等。
有人下場景,App 正在播放視頻,此時回到手機桌面或者切換到其他 App,那麼此時視頻應該暫停播放,Flutter 中使用 「AppLifecycleState」 實現:
class AppLifecycle extends StatefulWidget {
@override
_AppLifecycleState createState() => _AppLifecycleState();
}
class _AppLifecycleState extends State<AppLifecycle>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
//TODO
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('App 生命週期'),
),
body: Center(
child: Text(''),
),
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
重點是重寫 「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 個頁面 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('返回的參數');
});
}
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/7cla3VtqaKRzg0MZfxgr_A