Home Flutter页面的生命周期
Post
Cancel

Flutter页面的生命周期

结论

先上结果,Flutter页面的生命周期:

一个常规的StatefulWidget的生命周期

page create -> page state create -> page state init -> page state didChangeDependencies -> page state build -> {history pages} build -> …..-> page state dispose -> {history pages} build

比较诡异的是,不管是push新页面,还是pop当前页,history pages(不在栈顶的页面)都会重新build一下。为什么会这样,后面会详细说明。

启动app时,home page的生命周期

page create -> page state create -> page state init -> page state didChangeDependencies -> page state build -> page home create -> page state didUpdateWidget -> page state build

启动页page被创建了两次,而相应的state却没有,很明显地体现出了widget与element的差距了。

state.didUpdateWidget何时被调用

在调用了setState或者其他的情况,导致页面刷新时,某个子节点element的直接父节点会调用updateChild方法,判断该element是否能被复用。如可以,会调用element.update方法,该方法会调用state.didUpdateWidget.

state.didChangeDependencies何时被调用

  1. 一个新页面在push出来时,会调用到该方法

  2. 当屏幕方向改变或者语言发生变化时,会调用该方法。(针对这种情况,下面有详细说明。)

实验

页面push & pop

本次实验的页面结构:homepage -> statefulpage 1 -> statefulpage2 -> statefulpage3.

  1. 首先,启动app, 进入homepage
1
2
3
4
5
6
7
8
I/flutter (20505): Page Home create
I/flutter (20505): Page Home state create
I/flutter (20505): Page Home initState
I/flutter (20505): Page Home didChangeDependencies
I/flutter (20505): Page Home build
I/flutter (20505): Page Home create
I/flutter (20505): Page Home didUpdateWidget
I/flutter (20505): Page Home build
  1. homepage push 到 statefulpage 1
1
2
3
4
5
6
I/flutter (20505): Page StatefulWidget 1 create
I/flutter (20505): Page StatefulWidget 1 state create
I/flutter (20505): Page StatefulWidget 1 initState
I/flutter (20505): Page StatefulWidget 1 didChangeDependencies
I/flutter (20505): Page StatefulWidget 1 build
I/flutter (20505): Page Home build
  1. statefulpage 1 push 到 statefulpage 2
1
2
3
4
5
6
7
I/flutter (20505): Page StatefulWidget 2 create
I/flutter (20505): Page StatefulWidget 2 state create
I/flutter (20505): Page StatefulWidget 2 initState
I/flutter (20505): Page StatefulWidget 2 didChangeDependencies
I/flutter (20505): Page StatefulWidget 2 build
I/flutter (20505): Page Home build
I/flutter (20505): Page StatefulWidget 1 build
  1. statefulpage 2 push 到 statefulpage 3
1
2
3
4
5
6
7
8
I/flutter (20505): Page StatefulWidget 3 create
I/flutter (20505): Page StatefulWidget 3 state create
I/flutter (20505): Page StatefulWidget 3 initState
I/flutter (20505): Page StatefulWidget 3 didChangeDependencies
I/flutter (20505): Page StatefulWidget 3 build
I/flutter (20505): Page Home build
I/flutter (20505): Page StatefulWidget 2 build
I/flutter (20505): Page StatefulWidget 1 build
  1. 此时,hot reload一下
1
2
3
4
5
6
7
8
9
10
11
12
I/flutter (20505): Page Home create
I/flutter (20505): Page StatefulWidget 2 create
I/flutter (20505): Page StatefulWidget 2 didUpdateWidget
I/flutter (20505): Page StatefulWidget 2 build
I/flutter (20505): Page Home didUpdateWidget
I/flutter (20505): Page Home build
I/flutter (20505): Page StatefulWidget 3 create
I/flutter (20505): Page StatefulWidget 3 didUpdateWidget
I/flutter (20505): Page StatefulWidget 3 build
I/flutter (20505): Page StatefulWidget 1 create
I/flutter (20505): Page StatefulWidget 1 didUpdateWidget
I/flutter (20505): Page StatefulWidget 1 build
  1. statefulpage 3 pop
1
2
3
4
I/flutter (20505): Page Home build
I/flutter (20505): Page StatefulWidget 2 build
I/flutter (20505): Page StatefulWidget 1 build
I/flutter (20505): Page StatefulWidget 3 dispose
  1. statefulpage 2 pop
1
2
3
I/flutter (20505): Page Home build
I/flutter (20505): Page StatefulWidget 1 build
I/flutter (20505): Page StatefulWidget 2 dispose
  1. statefulpage 1 pop
1
2
I/flutter (20505): Page Home build
I/flutter (20505): Page StatefulWidget 1 dispose
  1. homepage pop
1
2
3
4
5
D/FlutterView(22514): Detaching from a FlutterEngine: io.flutter.embedding.engine.FlutterEngine@586b66e
D/FlutterActivityAndFragmentDelegate(22514): Detaching FlutterEngine from the Activity that owns this Fragment.
D/FlutterEngine(22514): Destroying.
D/FlutterEnginePluginRegistry(22514): Destroying.
W/libEGL  (22514): eglTerminate() called w/ 1 objects remaining

好像 home page的dispose方法没有被调用?

屏幕翻转

一般情况下,屏幕翻转不会触发任何的方法调用。

但是当你的build中,有判断屏幕方向时:

1
2
3
4
5
6
7
8
MaterialButton(
	color: MediaQuery.of(context).orientation ==  Orientation.portrait ? Colors.green : Colors.red,
	textColor: Colors.white,
	onPressed: () {
		Navigator.of(context).pushNamed("page32");
	},
	child: Text("跳转"),
)

这个时候,didChangeDependencies会被触发:

1
2
I/flutter (20505): Page StatefulWidget 2 didChangeDependencies
I/flutter (20505): Page StatefulWidget 2 build

一般情况下,widget树的父节点有InheritedWidget子类WidgetType1。当子widget有通过context.dependOnInheritedWidgetOfExactType(WidgetType1)获取到父节点WidgetType1, 并根据WidgetType1中的值进行显示等。此时,child便已经注册监听了WidgetType1。当WidgetType1改变且WidgetType1.updateShouldNotify == true时,会通知这个child, 触发childState.didChangeDependencies方法,最后触发childState.build

TabBar 控件

  1. 默认在tab 1页面
1
2
3
4
5
6
I/flutter (20505): tab 1 create
I/flutter (20505): tab 2 create
I/flutter (20505): tab 1 state create
I/flutter (20505): tab 1 initState
I/flutter (20505): tab 1 didChangeDependencies
I/flutter (20505): tab 1 build
  1. 点击 tab 2
1
2
3
4
5
I/flutter (20505): tab 2 state create
I/flutter (20505): tab 2 initState
I/flutter (20505): tab 2 didChangeDependencies
I/flutter (20505): tab 2 build
I/flutter (20505): tab 1 dispose
  1. 点击 tab 1
1
2
3
4
5
I/flutter (20505): tab 1 state create
I/flutter (20505): tab 1 initState
I/flutter (20505): tab 1 didChangeDependencies
I/flutter (20505): tab 1 build
I/flutter (20505): tab 2 dispose
  1. hot reload
1
2
3
4
I/flutter (20505): tab 1 create
I/flutter (20505): tab 2 create
I/flutter (20505): tab 1 didUpdateWidget
I/flutter (20505): tab 1 build

切换相同控件

如下面的代码:

1
random == 0 ? TabViewWidget("widget 1", Colors.green) : TabViewWidget("widget 2", Colors.red)
  1. 具体日志
1
2
3
4
5
6
7
8
9
I/flutter (20505): widget 1 create
I/flutter (20505): widget 1 state create
I/flutter (20505): widget 1 initState
I/flutter (20505): widget 1 didChangeDependencies
I/flutter (20505): widget 1 build

I/flutter (20505): widget 2 create
I/flutter (20505): widget 2 didUpdateWidget
I/flutter (20505): widget 2 build

上面有个遗留问题,为什么在push新页面时,之前的页面都要重新build一次?从Navigator的源码中,尝试寻找答案。

在这之前,先了解下WidgetsApp路由的优先级:home > routes > onGenerateRoute

home的路由,对应的routeName是”/”

WidgetsApp 的 路由配置:home/routes/onGenenrateRoute, 最后都会转换成 Navigator的onGenenrateRoute。(源码比较清晰,这里不展开)

Navigator.pushName, 最后会调用NavigatorState.push方法,它的 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Future<T> push<T extends Object>(Route<T> route) {
    assert(!_debugLocked);
    assert(() {
      _debugLocked = true;
      return true;
    }());
    assert(route != null);
    assert(route._navigator == null);
    final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
    route._navigator = this;
    route.install(_currentOverlayEntry);
    _history.add(route);
    route.didPush();
    route.didChangeNext(null);
    if (oldRoute != null) {
      oldRoute.didChangeNext(route);
      route.didChangePrevious(oldRoute);
    }
    for (NavigatorObserver observer in widget.observers)
      observer.didPush(route, oldRoute);
    RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, oldRoute);
    assert(() {
      _debugLocked = false;
      return true;
    }());
    _afterNavigation(route);
    return route.popped;
  }

在看源码之前,先了解下入参Route类:

MaterialPageRoute <- PageRoute <- ModalRoute <- TransitionRoute<- OverlayRoute <- Route

常用的是 MaterialPageRouteCupertinoPageRoute, 应该都很熟悉。

关于ModalRoute, 也有两点需要先介绍下:

  • ModalRoute.maintainState

    是否保存不可见的页面,即被push之后处于当前页面之下的页面。若true, 不可见页面的state会被保存。若false,不可见页面的数据都不会被保存。

    默认情况下,MaterialPageRoute、CupertinoPageRoute的maintainState均为true。

  • ModalRoute一般会有两中OverlayEntryModalBarrier_ModalScope。其中,ModalBarrier负责背景的绘制,如dialog的半透明背景。_ModalScope负责绘制page。

接着,来分析这段中的部分源码:

1
2
3
4
5
6
7
8
9
10
route.install(_currentOverlayEntry);

//其他关联代码
OverlayEntry get _currentOverlayEntry {
    for (Route<dynamic> route in _history.reversed) {
        if (route.overlayEntries.isNotEmpty)
            return route.overlayEntries.last;
    }
    return null;
}

_currentOverlayEntry这个就是在_history中的最后一个overlayEntries.

route.install方法在OverlayRoute以及TransitionRoute都有新增相关调用:

OverlayRoute中,

route.install时将route._overlayEntries 插入到NavigatorState_entries中,并且NavigatorState.installAll会触发重新build. 这里的route._overlayEntries就是上面提到的ModalRoute中的两个OverlayEntry.

TransitionRoute中,

route.install时创建AnimationControllerAnimation.

1
_history.add(route);

这句比较好理解,将新的route添加到_history队列。

1
2
route.didPush();
route.didChangeNext(null);

TransitionRoute中,

route.didPush时启动动画Animation.forward, 过场动画就是从这里开始的。TransitionRoute.didPush会调用到_didPushOrReplace, 该方法会监控Animation动画的状态。

route.didChangeNext, 用来设置SecondaryAnimation.

1
route.didChangePrevious(oldRoute);

ModalRoute 中,

route.didChangePrevious, 调用changedInternalState.

route.changedInternalState, 一是调用_ModalScope.setState, 二是ModalBarrier.markNeedsBuild。其实就是让两个OverlayEntry重新build. 而_ModalScope的build方法会调用route.buildTransitions方法以及route.buildPage方法。(这两个方法应该不陌生,在自定义过场动画时会用到)

栈下的page会触发build的原因

TransitionRoute中,disPush-> _didPushOrReplace ->监听Animation状态,在AnimationStatus.reverse时, opaque = false, 表示栈顶的前一页被允许绘制出来,此时正好开始了过场动画。在AnimationStatus.completed时,opaque = true, 表示栈顶下的所有页面都不会被绘制,此时正好是过场动画结束。在设置opaque时,overlayEntries.first.opaque = opaque, 回调用_overlay._didChangeEntryOpacity,这个_overlay就是Navigator.overlay。该方法如下:

1
2
3
4
5
6
void _didChangeEntryOpacity() {
    setState(() {
      // We use the opacity of the entry in our build function, which means we
      // our state has changed.
    });
  }

然后,OverlayState.build方法触发,所有的_entries都会被涉及到,导致之前的page.build

This post is licensed under CC BY 4.0 by the author.