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('返回的參數');
    });
  }
}

  

 

Tags: