zoukankan      html  css  js  c++  java
  • 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('返回的参数');
        });
      }
    }

      

  • 相关阅读:
    Django使用redis
    Django中static media的简单配置
    套接字,TCP,UDP
    nginx常用配置
    使用systemctl管理nginx
    jumpserver 安装
    elasticsearch7.x集群安装(含head、bigdesk、kibana插件)
    codepush安装
    mysql优化后的主配置文件
    nginx优化、负载均衡、rewrite
  • 原文地址:https://www.cnblogs.com/mukekeheart/p/14338842.html
Copyright © 2011-2022 走看看