结论
先上结果,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何时被调用
一个新页面在push出来时,会调用到该方法
当屏幕方向改变或者语言发生变化时,会调用该方法。(针对这种情况,下面有详细说明。)
实验
页面push & pop
本次实验的页面结构:homepage -> statefulpage 1 -> statefulpage2 -> statefulpage3.
- 首先,启动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
- 从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
- 从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
- 从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
- 此时,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
- 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
- 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
- statefulpage 1 pop
1
2
I/flutter (20505): Page Home build
I/flutter (20505): Page StatefulWidget 1 dispose
- 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 控件
- 默认在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
- 点击 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
- 点击 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
- 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
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
Navigator
上面有个遗留问题,为什么在push新页面时,之前的页面都要重新build一次?从Navigator
的源码中,尝试寻找答案。
在这之前,先了解下WidgetsApp
路由的优先级:home > routes > onGenerateRoute
home的路由,对应的routeName是”/”
而WidgetsApp
的 路由配置:home/routes/onGenenrateRoute, 最后都会转换成 Navigator的onGenenrateRoute。(源码比较清晰,这里不展开)
NavigatorState.push
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
常用的是 MaterialPageRoute
、CupertinoPageRoute
, 应该都很熟悉。
关于ModalRoute
, 也有两点需要先介绍下:
ModalRoute.maintainState
是否保存不可见的页面,即被push之后处于当前页面之下的页面。若true, 不可见页面的state会被保存。若false,不可见页面的数据都不会被保存。
默认情况下,MaterialPageRoute、CupertinoPageRoute的
maintainState
均为true。ModalRoute一般会有两中OverlayEntry:
ModalBarrier
、_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时创建AnimationController
、Animation
.
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。